@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.
Files changed (180) hide show
  1. package/README.md +68 -0
  2. package/agent/commands/acp.clarification-address.md +417 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +437 -0
  5. package/agent/commands/acp.clarifications-research.md +326 -0
  6. package/agent/commands/acp.command-create.md +432 -0
  7. package/agent/commands/acp.design-create.md +286 -0
  8. package/agent/commands/acp.design-reference.md +355 -0
  9. package/agent/commands/acp.handoff.md +270 -0
  10. package/agent/commands/acp.index.md +423 -0
  11. package/agent/commands/acp.init.md +546 -0
  12. package/agent/commands/acp.package-create.md +895 -0
  13. package/agent/commands/acp.package-info.md +212 -0
  14. package/agent/commands/acp.package-install.md +539 -0
  15. package/agent/commands/acp.package-list.md +280 -0
  16. package/agent/commands/acp.package-publish.md +541 -0
  17. package/agent/commands/acp.package-remove.md +293 -0
  18. package/agent/commands/acp.package-search.md +307 -0
  19. package/agent/commands/acp.package-update.md +361 -0
  20. package/agent/commands/acp.package-validate.md +540 -0
  21. package/agent/commands/acp.pattern-create.md +386 -0
  22. package/agent/commands/acp.plan.md +587 -0
  23. package/agent/commands/acp.proceed.md +882 -0
  24. package/agent/commands/acp.project-create.md +675 -0
  25. package/agent/commands/acp.project-info.md +312 -0
  26. package/agent/commands/acp.project-list.md +226 -0
  27. package/agent/commands/acp.project-remove.md +379 -0
  28. package/agent/commands/acp.project-set.md +227 -0
  29. package/agent/commands/acp.project-update.md +307 -0
  30. package/agent/commands/acp.projects-restore.md +228 -0
  31. package/agent/commands/acp.projects-sync.md +347 -0
  32. package/agent/commands/acp.report.md +407 -0
  33. package/agent/commands/acp.resume.md +239 -0
  34. package/agent/commands/acp.sessions.md +301 -0
  35. package/agent/commands/acp.status.md +293 -0
  36. package/agent/commands/acp.sync.md +364 -0
  37. package/agent/commands/acp.task-create.md +500 -0
  38. package/agent/commands/acp.update.md +302 -0
  39. package/agent/commands/acp.validate.md +466 -0
  40. package/agent/commands/acp.version-check-for-updates.md +276 -0
  41. package/agent/commands/acp.version-check.md +191 -0
  42. package/agent/commands/acp.version-update.md +289 -0
  43. package/agent/commands/command.template.md +339 -0
  44. package/agent/commands/git.commit.md +526 -0
  45. package/agent/commands/git.init.md +514 -0
  46. package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
  47. package/agent/commands/tanstack-cloudflare.tail.md +275 -0
  48. package/agent/design/.gitkeep +0 -0
  49. package/agent/design/design.template.md +154 -0
  50. package/agent/design/local.dashboard-layout-routing.md +288 -0
  51. package/agent/design/local.data-model-yaml-parsing.md +310 -0
  52. package/agent/design/local.search-filtering.md +331 -0
  53. package/agent/design/local.server-api-auto-refresh.md +235 -0
  54. package/agent/design/local.table-tree-views.md +299 -0
  55. package/agent/design/local.visualizer-requirements.md +349 -0
  56. package/agent/design/requirements.template.md +387 -0
  57. package/agent/index/.gitkeep +0 -0
  58. package/agent/index/acp.core.yaml +137 -0
  59. package/agent/index/local.main.template.yaml +37 -0
  60. package/agent/manifest.template.yaml +13 -0
  61. package/agent/manifest.yaml +302 -0
  62. package/agent/milestones/.gitkeep +0 -0
  63. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
  64. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  65. package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
  66. package/agent/package.template.yaml +86 -0
  67. package/agent/patterns/.gitkeep +0 -0
  68. package/agent/patterns/bootstrap.template.md +1237 -0
  69. package/agent/patterns/pattern.template.md +382 -0
  70. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
  71. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
  72. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
  73. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
  74. package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
  75. package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
  76. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
  77. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
  78. package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
  79. package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
  80. package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
  81. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
  82. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
  83. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
  84. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
  85. package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
  86. package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
  87. package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
  88. package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
  89. package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
  90. package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
  91. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
  92. package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
  93. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
  94. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
  95. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
  96. package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
  97. package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
  98. package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
  99. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
  100. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
  101. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
  102. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
  103. package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
  104. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
  105. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
  106. package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
  107. package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
  108. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
  109. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
  110. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
  111. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
  112. package/agent/progress.template.yaml +161 -0
  113. package/agent/progress.yaml +145 -0
  114. package/agent/schemas/package.schema.yaml +276 -0
  115. package/agent/scripts/acp.common.sh +1781 -0
  116. package/agent/scripts/acp.install.sh +333 -0
  117. package/agent/scripts/acp.package-create.sh +924 -0
  118. package/agent/scripts/acp.package-info.sh +288 -0
  119. package/agent/scripts/acp.package-install.sh +893 -0
  120. package/agent/scripts/acp.package-list.sh +311 -0
  121. package/agent/scripts/acp.package-publish.sh +420 -0
  122. package/agent/scripts/acp.package-remove.sh +348 -0
  123. package/agent/scripts/acp.package-search.sh +156 -0
  124. package/agent/scripts/acp.package-update.sh +517 -0
  125. package/agent/scripts/acp.package-validate.sh +1018 -0
  126. package/agent/scripts/acp.uninstall.sh +85 -0
  127. package/agent/scripts/acp.version-check-for-updates.sh +98 -0
  128. package/agent/scripts/acp.version-check.sh +47 -0
  129. package/agent/scripts/acp.version-update.sh +176 -0
  130. package/agent/scripts/acp.yaml-parser.sh +985 -0
  131. package/agent/scripts/acp.yaml-validate.sh +205 -0
  132. package/agent/tasks/.gitkeep +0 -0
  133. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
  134. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
  135. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
  136. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
  137. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
  138. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
  139. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
  140. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
  141. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
  142. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
  143. package/agent/tasks/task-1-{title}.template.md +244 -0
  144. package/bin/visualize.mjs +84 -0
  145. package/package.json +48 -0
  146. package/src/components/ExtraFieldsBadge.tsx +15 -0
  147. package/src/components/FilterBar.tsx +33 -0
  148. package/src/components/Header.tsx +23 -0
  149. package/src/components/MilestoneTable.tsx +167 -0
  150. package/src/components/MilestoneTree.tsx +84 -0
  151. package/src/components/ProgressBar.tsx +20 -0
  152. package/src/components/SearchInput.tsx +22 -0
  153. package/src/components/Sidebar.tsx +54 -0
  154. package/src/components/StatusBadge.tsx +23 -0
  155. package/src/components/StatusDot.tsx +12 -0
  156. package/src/components/TaskList.tsx +36 -0
  157. package/src/components/ViewToggle.tsx +31 -0
  158. package/src/lib/config.ts +8 -0
  159. package/src/lib/file-watcher.ts +43 -0
  160. package/src/lib/search.ts +48 -0
  161. package/src/lib/types.ts +73 -0
  162. package/src/lib/useAutoRefresh.ts +31 -0
  163. package/src/lib/useCollapse.ts +31 -0
  164. package/src/lib/useFilteredData.ts +55 -0
  165. package/src/lib/yaml-loader-real.spec.ts +47 -0
  166. package/src/lib/yaml-loader.spec.ts +201 -0
  167. package/src/lib/yaml-loader.ts +265 -0
  168. package/src/routeTree.gen.ts +140 -0
  169. package/src/router.tsx +10 -0
  170. package/src/routes/__root.tsx +75 -0
  171. package/src/routes/api/watch.ts +29 -0
  172. package/src/routes/index.tsx +115 -0
  173. package/src/routes/milestones.tsx +50 -0
  174. package/src/routes/search.tsx +84 -0
  175. package/src/routes/tasks.tsx +63 -0
  176. package/src/services/progress-database.service.ts +46 -0
  177. package/src/styles.css +25 -0
  178. package/tsconfig.json +24 -0
  179. package/vite.config.ts +16 -0
  180. package/vitest.config.ts +27 -0
@@ -0,0 +1,516 @@
1
+ # Durable Objects WebSocket Pattern
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: TanStack Start + Cloudflare Workers applications requiring real-time communication
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Cloudflare Durable Objects provide stateful, long-lived server instances that are ideal for WebSocket-based real-time features. This pattern demonstrates how to build a Durable Object that accepts WebSocket connections, processes typed messages, and delegates business logic to an injected engine using dependency injection.
12
+
13
+ The Durable Object acts as a thin coordination layer — it manages WebSocket lifecycle (accept, message routing, close) while delegating all domain logic to portable engine classes. This separation enables the business logic to be tested independently and potentially extracted into reusable packages.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Building real-time features (chat, collaboration, live updates)
21
+ - Need persistent server-side state across requests
22
+ - Want WebSocket connections managed at the edge
23
+ - Need to coordinate between multiple connected clients
24
+ - Building features that require streaming responses (AI chat, file upload progress)
25
+
26
+ ❌ **Don't use this pattern when:**
27
+ - Simple request/response patterns suffice (use API routes instead)
28
+ - No real-time requirement (polling or SSR preloading is simpler)
29
+ - Stateless operations (use regular Workers)
30
+
31
+ ---
32
+
33
+ ## Core Principles
34
+
35
+ 1. **Thin Wrapper**: The Durable Object is a thin coordination layer — it manages WebSocket lifecycle and delegates business logic to injected engines
36
+ 2. **Dependency Injection**: All providers (AI, storage, etc.) are injected into the engine at construction time
37
+ 3. **Typed Messages**: Both client and server messages use TypeScript interfaces with discriminated `type` fields
38
+ 4. **Persistent Provider Instances**: Provider instances are created once in the constructor and reused across messages for caching benefits
39
+ 5. **Graceful Error Handling**: Errors are caught and sent back to the client as error messages, never crashing the Durable Object
40
+ 6. **WebSocket Pair Pattern**: Use Cloudflare's `WebSocketPair` API for server-accepted WebSocket connections
41
+
42
+ ---
43
+
44
+ ## Implementation
45
+
46
+ ### Structure
47
+
48
+ ```
49
+ src/
50
+ ├── durable-objects/
51
+ │ ├── ChatRoom.ts # Durable Object (thin wrapper)
52
+ │ └── UploadManager.ts # Another Durable Object example
53
+ ├── lib/
54
+ │ ├── chat/
55
+ │ │ ├── chat-engine.ts # Core business logic (portable)
56
+ │ │ └── interfaces/ # Provider interfaces
57
+ │ └── chat-providers/
58
+ │ ├── ai-provider.ts # AI provider implementation
59
+ │ └── storage-provider.ts # Storage provider implementation
60
+ └── routes/
61
+ └── api/
62
+ └── chat-ws.tsx # WebSocket upgrade endpoint
63
+ ```
64
+
65
+ ### Code Example
66
+
67
+ #### Step 1: Define Typed Messages
68
+
69
+ ```typescript
70
+ // src/durable-objects/types.ts
71
+
72
+ /** Messages sent from client → server */
73
+ interface ClientMessage {
74
+ type: 'message' | 'load_messages' | 'init'
75
+ userId: string
76
+ conversationId?: string
77
+ message?: MessageContent
78
+ limit?: number
79
+ startAfter?: string
80
+ }
81
+
82
+ /** Messages sent from server → client */
83
+ interface ServerMessage {
84
+ type: 'chunk' | 'complete' | 'error' | 'message' | 'messages_loaded' | 'ready'
85
+ content?: string
86
+ error?: string
87
+ message?: any
88
+ messages?: any[]
89
+ hasMore?: boolean
90
+ }
91
+ ```
92
+
93
+ #### Step 2: Create the Durable Object
94
+
95
+ ```typescript
96
+ // src/durable-objects/ChatRoom.ts
97
+ import { DurableObject } from 'cloudflare:workers'
98
+ import { ChatEngine } from '@/lib/chat'
99
+ import { BedrockAIProvider, FirebaseStorageProvider } from '@/lib/chat-providers'
100
+ import { chatLogger } from '@/lib/logger'
101
+
102
+ export class ChatRoom extends DurableObject {
103
+ private sessions: Set<WebSocket>
104
+ private chatEngine: ChatEngine
105
+
106
+ constructor(state: DurableObjectState, env: Env) {
107
+ super(state, env)
108
+ this.sessions = new Set()
109
+
110
+ // Inject dependencies into engine (created once, reused across messages)
111
+ this.chatEngine = new ChatEngine({
112
+ aiProvider: new BedrockAIProvider(),
113
+ storageProvider: new FirebaseStorageProvider(),
114
+ logger: chatLogger
115
+ })
116
+ }
117
+
118
+ async fetch(request: Request): Promise<Response> {
119
+ // Verify WebSocket upgrade
120
+ const upgradeHeader = request.headers.get('Upgrade')
121
+ if (upgradeHeader !== 'websocket') {
122
+ return new Response('Expected WebSocket', { status: 426 })
123
+ }
124
+
125
+ // Create WebSocket pair
126
+ const pair = new WebSocketPair()
127
+ const [client, server] = Object.values(pair)
128
+
129
+ // Accept and track connection
130
+ server.accept()
131
+ this.sessions.add(server)
132
+
133
+ // Handle messages
134
+ server.addEventListener('message', async (event) => {
135
+ try {
136
+ const data = JSON.parse(event.data as string) as ClientMessage
137
+
138
+ switch (data.type) {
139
+ case 'init':
140
+ this.sendMessage(server, { type: 'ready' })
141
+ break
142
+ case 'message':
143
+ await this.handleMessage(data, server)
144
+ break
145
+ case 'load_messages':
146
+ await this.handleLoadMessages(data, server)
147
+ break
148
+ default:
149
+ chatLogger.warn('Unknown message type', { type: (data as any).type })
150
+ }
151
+ } catch (error) {
152
+ this.sendMessage(server, {
153
+ type: 'error',
154
+ error: error instanceof Error ? error.message : 'Unknown error'
155
+ })
156
+ }
157
+ })
158
+
159
+ // Handle close
160
+ server.addEventListener('close', () => {
161
+ this.sessions.delete(server)
162
+ })
163
+
164
+ return new Response(null, { status: 101, webSocket: client })
165
+ }
166
+
167
+ private async handleMessage(data: ClientMessage, socket: WebSocket): Promise<void> {
168
+ const { userId, conversationId = 'main', message } = data
169
+
170
+ if (!message) {
171
+ this.sendMessage(socket, { type: 'error', error: 'No message provided' })
172
+ return
173
+ }
174
+
175
+ // Delegate to engine with streaming callback
176
+ await this.chatEngine.processMessage({
177
+ userId,
178
+ conversationId,
179
+ message,
180
+ onMessage: (msg) => {
181
+ this.sendMessage(socket, msg)
182
+ }
183
+ })
184
+ }
185
+
186
+ private async handleLoadMessages(data: ClientMessage, socket: WebSocket): Promise<void> {
187
+ const { userId, conversationId = 'main', limit = 50, startAfter } = data
188
+
189
+ try {
190
+ const messages = await this.chatEngine.loadMessages({
191
+ userId, conversationId, limit, startAfter
192
+ })
193
+
194
+ this.sendMessage(socket, {
195
+ type: 'messages_loaded',
196
+ messages,
197
+ hasMore: messages.length === limit
198
+ })
199
+ } catch (error) {
200
+ this.sendMessage(socket, {
201
+ type: 'error',
202
+ error: error instanceof Error ? error.message : 'Failed to load messages'
203
+ })
204
+ }
205
+ }
206
+
207
+ private sendMessage(socket: WebSocket, message: ServerMessage): void {
208
+ try {
209
+ socket.send(JSON.stringify(message))
210
+ } catch (error) {
211
+ console.error('Failed to send WebSocket message', error)
212
+ }
213
+ }
214
+ }
215
+ ```
216
+
217
+ #### Step 3: Configure Wrangler
218
+
219
+ ```toml
220
+ # wrangler.toml
221
+
222
+ # Durable Object bindings
223
+ [[durable_objects.bindings]]
224
+ name = "CHAT_ROOM"
225
+ class_name = "ChatRoom"
226
+
227
+ # Migrations
228
+ [[migrations]]
229
+ tag = "v1"
230
+ new_sqlite_classes = ["ChatRoom"]
231
+ ```
232
+
233
+ #### Step 4: Create WebSocket Upgrade Route
234
+
235
+ ```typescript
236
+ // src/routes/api/chat-ws.tsx
237
+ import { createFileRoute } from '@tanstack/react-router'
238
+ import { getAuthSession } from '@/lib/auth/server-fn'
239
+
240
+ export const Route = createFileRoute('/api/chat-ws')({
241
+ server: {
242
+ handlers: {
243
+ GET: async ({ request, context }) => {
244
+ const user = await getAuthSession()
245
+ if (!user) {
246
+ return new Response('Unauthorized', { status: 401 })
247
+ }
248
+
249
+ // Get Durable Object stub
250
+ const env = context.cloudflare.env as Env
251
+ const id = env.CHAT_ROOM.idFromName(user.uid)
252
+ const stub = env.CHAT_ROOM.get(id)
253
+
254
+ // Forward WebSocket upgrade to Durable Object
255
+ return stub.fetch(request)
256
+ },
257
+ },
258
+ },
259
+ })
260
+ ```
261
+
262
+ #### Step 5: Client-Side WebSocket Hook
263
+
264
+ ```typescript
265
+ // src/hooks/use-websocket.ts
266
+ import { useState, useEffect, useCallback, useRef } from 'react'
267
+
268
+ interface UseWebSocketOptions {
269
+ userId: string
270
+ onMessage: (message: ServerMessage) => void
271
+ onConnectionChange?: (connected: boolean) => void
272
+ }
273
+
274
+ export function useWebSocket({ userId, onMessage, onConnectionChange }: UseWebSocketOptions) {
275
+ const wsRef = useRef<WebSocket | null>(null)
276
+ const [connected, setConnected] = useState(false)
277
+
278
+ useEffect(() => {
279
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
280
+ const ws = new WebSocket(`${protocol}//${window.location.host}/api/chat-ws`)
281
+ wsRef.current = ws
282
+
283
+ ws.onopen = () => {
284
+ setConnected(true)
285
+ onConnectionChange?.(true)
286
+ ws.send(JSON.stringify({ type: 'init', userId }))
287
+ }
288
+
289
+ ws.onmessage = (event) => {
290
+ const data = JSON.parse(event.data)
291
+ onMessage(data)
292
+ }
293
+
294
+ ws.onclose = () => {
295
+ setConnected(false)
296
+ onConnectionChange?.(false)
297
+ }
298
+
299
+ return () => ws.close()
300
+ }, [userId])
301
+
302
+ const sendMessage = useCallback((message: ClientMessage) => {
303
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
304
+ wsRef.current.send(JSON.stringify(message))
305
+ }
306
+ }, [])
307
+
308
+ return { connected, sendMessage }
309
+ }
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Examples
315
+
316
+ ### Example 1: File Upload Durable Object
317
+
318
+ ```typescript
319
+ // src/durable-objects/UploadManager.ts
320
+ import { DurableObject } from 'cloudflare:workers'
321
+
322
+ export class UploadManager extends DurableObject {
323
+ async fetch(request: Request): Promise<Response> {
324
+ if (request.headers.get('Upgrade') !== 'websocket') {
325
+ return new Response('Expected WebSocket', { status: 426 })
326
+ }
327
+
328
+ const pair = new WebSocketPair()
329
+ const [client, server] = Object.values(pair)
330
+ server.accept()
331
+
332
+ server.addEventListener('message', async (event) => {
333
+ try {
334
+ const data = JSON.parse(event.data as string)
335
+
336
+ switch (data.type) {
337
+ case 'upload_chunk':
338
+ // Process chunk, report progress
339
+ server.send(JSON.stringify({
340
+ type: 'progress',
341
+ progress: data.chunkIndex / data.totalChunks * 100
342
+ }))
343
+ break
344
+ case 'upload_complete':
345
+ // Finalize upload
346
+ server.send(JSON.stringify({ type: 'complete', url: signedUrl }))
347
+ break
348
+ }
349
+ } catch (error) {
350
+ server.send(JSON.stringify({ type: 'error', error: String(error) }))
351
+ }
352
+ })
353
+
354
+ return new Response(null, { status: 101, webSocket: client })
355
+ }
356
+ }
357
+ ```
358
+
359
+ ### Example 2: Internal HTTP Endpoint on Durable Object
360
+
361
+ ```typescript
362
+ // Durable Objects can also handle non-WebSocket HTTP requests
363
+ export class ChatRoom extends DurableObject {
364
+ async fetch(request: Request): Promise<Response> {
365
+ const url = new URL(request.url)
366
+
367
+ // Internal cache invalidation endpoint
368
+ if (url.pathname === '/invalidate-cache' && request.method === 'POST') {
369
+ const body = await request.json()
370
+ this.mcpProvider.clearCache()
371
+ return Response.json({ success: true })
372
+ }
373
+
374
+ // WebSocket upgrade (normal flow)
375
+ if (request.headers.get('Upgrade') === 'websocket') {
376
+ return this.handleWebSocket(request)
377
+ }
378
+
379
+ return new Response('Expected WebSocket or known endpoint', { status: 426 })
380
+ }
381
+ }
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Benefits
387
+
388
+ ### 1. Persistent State Across Messages
389
+ Durable Objects maintain state between requests, enabling caching of provider instances and connection state.
390
+
391
+ ### 2. Edge-Based Real-Time
392
+ WebSocket connections run at Cloudflare's edge, providing low-latency real-time communication globally.
393
+
394
+ ### 3. Testable Business Logic
395
+ Business logic lives in injected engines, not in the Durable Object itself, making it independently testable.
396
+
397
+ ### 4. Automatic Scaling
398
+ Each Durable Object instance is uniquely addressable (by user ID, room ID, etc.) and scales automatically.
399
+
400
+ ---
401
+
402
+ ## Trade-offs
403
+
404
+ ### 1. Durable Object Limitations
405
+ **Downside**: Each Durable Object runs in a single location and has CPU/memory limits.
406
+ **Mitigation**: Keep Durable Objects thin. Delegate heavy work to external services.
407
+
408
+ ### 2. Cold Start Latency
409
+ **Downside**: First request to a Durable Object may have cold start delay.
410
+ **Mitigation**: Use `init` messages to pre-warm connections after WebSocket establishes.
411
+
412
+ ---
413
+
414
+ ## Anti-Patterns
415
+
416
+ ### ❌ Anti-Pattern 1: Business Logic in Durable Object
417
+
418
+ ```typescript
419
+ // ❌ BAD: Business logic mixed into Durable Object
420
+ export class ChatRoom extends DurableObject {
421
+ async handleMessage(data, socket) {
422
+ const response = await fetch('https://api.anthropic.com/...') // Direct API call
423
+ const messages = await getDocument(...) // Direct DB call
424
+ // ... 200 lines of business logic
425
+ }
426
+ }
427
+
428
+ // ✅ GOOD: Delegate to engine
429
+ export class ChatRoom extends DurableObject {
430
+ async handleMessage(data, socket) {
431
+ await this.chatEngine.processMessage({
432
+ ...data,
433
+ onMessage: (msg) => this.sendMessage(socket, msg)
434
+ })
435
+ }
436
+ }
437
+ ```
438
+
439
+ ### ❌ Anti-Pattern 2: Creating Providers Per Message
440
+
441
+ ```typescript
442
+ // ❌ BAD: New provider instances per message (loses caching)
443
+ server.addEventListener('message', async (event) => {
444
+ const aiProvider = new BedrockAIProvider() // Created every message!
445
+ const engine = new ChatEngine({ aiProvider })
446
+ })
447
+
448
+ // ✅ GOOD: Create once in constructor
449
+ constructor(state, env) {
450
+ this.chatEngine = new ChatEngine({
451
+ aiProvider: new BedrockAIProvider() // Created once, reused
452
+ })
453
+ }
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Testing Strategy
459
+
460
+ ### Unit Testing Engine (Without Durable Object)
461
+
462
+ ```typescript
463
+ describe('ChatEngine', () => {
464
+ it('should process messages', async () => {
465
+ const mockAI = { streamChat: jest.fn() }
466
+ const mockStorage = { saveMessage: jest.fn(), loadMessages: jest.fn() }
467
+
468
+ const engine = new ChatEngine({
469
+ aiProvider: mockAI,
470
+ storageProvider: mockStorage
471
+ })
472
+
473
+ const messages: any[] = []
474
+ await engine.processMessage({
475
+ userId: 'user1',
476
+ conversationId: 'conv1',
477
+ message: 'Hello',
478
+ onMessage: (msg) => messages.push(msg)
479
+ })
480
+
481
+ expect(mockStorage.saveMessage).toHaveBeenCalled()
482
+ })
483
+ })
484
+ ```
485
+
486
+ ---
487
+
488
+ ## Related Patterns
489
+
490
+ - **[Provider Adapter Pattern](./tanstack-cloudflare.provider-adapter.md)**: Interfaces for dependency injection into engines
491
+ - **[Rate Limiting Pattern](./tanstack-cloudflare.rate-limiting.md)**: Rate limit WebSocket connections
492
+ - **[Wrangler Configuration](./tanstack-cloudflare.wrangler-configuration.md)**: Durable Object bindings and migrations
493
+
494
+ ---
495
+
496
+ ## Checklist for Implementation
497
+
498
+ - [ ] Durable Object extends `DurableObject` from `cloudflare:workers`
499
+ - [ ] Business logic delegated to injected engine
500
+ - [ ] Provider instances created once in constructor
501
+ - [ ] Typed `ClientMessage` and `ServerMessage` interfaces
502
+ - [ ] `switch` statement on `data.type` for message routing
503
+ - [ ] Error handling wraps every message handler
504
+ - [ ] Errors sent to client as `{ type: 'error', error: string }`
505
+ - [ ] WebSocket pair created with `new WebSocketPair()`
506
+ - [ ] Server socket accepted with `server.accept()`
507
+ - [ ] Sessions tracked in `Set<WebSocket>` and cleaned up on close
508
+ - [ ] Wrangler configured with DO bindings and migrations
509
+ - [ ] WebSocket upgrade route validates auth before forwarding
510
+
511
+ ---
512
+
513
+ **Status**: Stable - Proven pattern for real-time features on Cloudflare Workers
514
+ **Recommendation**: Use for all real-time features requiring persistent server-side state
515
+ **Last Updated**: 2026-02-28
516
+ **Contributors**: Patrick Michaelsen