@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,237 @@
|
|
|
1
|
+
# WebSocket Manager
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: Real-time WebSocket connections with auto-reconnect, visibility recovery, and discriminated union message types
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A class-based WebSocket client (`ChatWebSocket`) with exponential backoff reconnection (5 attempts, 1s base), page visibility recovery, discriminated union message types, and init-based pre-warming. The server side uses a Cloudflare Durable Object (`ChatRoom`) for session multiplexing with ACL-filtered broadcasting. Each component instance creates its own WebSocket with proper message handlers.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### ChatWebSocket Client
|
|
18
|
+
|
|
19
|
+
**File**: `src/lib/chat/websocket.ts`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface ChatWebSocketConfig {
|
|
23
|
+
userId: string
|
|
24
|
+
conversationId?: string
|
|
25
|
+
ghostOwner?: string
|
|
26
|
+
onMessage: (message: WebSocketMessage) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ChatWebSocket {
|
|
30
|
+
private ws: WebSocket | null = null
|
|
31
|
+
private reconnectAttempts = 0
|
|
32
|
+
private maxReconnectAttempts = 5
|
|
33
|
+
private reconnectDelay = 1000 // Base delay (ms)
|
|
34
|
+
private intentionalDisconnect = false
|
|
35
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
private visibilityHandler: (() => void) | null = null
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Connection Lifecycle
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
connect()
|
|
44
|
+
├─ Set intentionalDisconnect = false
|
|
45
|
+
├─ Clean up old visibility handler
|
|
46
|
+
├─ Cancel pending reconnect timer
|
|
47
|
+
├─ Close zombie WebSocket (clear handlers first)
|
|
48
|
+
├─ Construct URL: wss://{host}/api/chat-ws?userId=X&conversationId=Y
|
|
49
|
+
└─ Create new WebSocket
|
|
50
|
+
├─ onopen:
|
|
51
|
+
│ ├─ Reset reconnectAttempts = 0
|
|
52
|
+
│ ├─ Emit connection_change { connected: true }
|
|
53
|
+
│ └─ Send init message (triggers server pre-warming)
|
|
54
|
+
├─ onmessage: Parse JSON → handleMessage(data)
|
|
55
|
+
├─ onerror: Emit error event
|
|
56
|
+
└─ onclose:
|
|
57
|
+
├─ Emit connection_change { connected: false }
|
|
58
|
+
├─ Register visibilitychange handler
|
|
59
|
+
└─ attemptReconnect()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Reconnection (Exponential Backoff)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
private attemptReconnect() {
|
|
66
|
+
if (this.intentionalDisconnect) return
|
|
67
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
68
|
+
this.config.onMessage({ type: 'error', error: 'Failed to reconnect' })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
this.reconnectAttempts++
|
|
72
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
|
73
|
+
// Sequence: 1s, 2s, 4s, 8s, 16s → give up
|
|
74
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Visibility Recovery
|
|
79
|
+
|
|
80
|
+
When the tab becomes visible and the WebSocket is dead, reconnect immediately with reset retry counter:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
this.visibilityHandler = () => {
|
|
84
|
+
if (document.visibilityState === 'visible' &&
|
|
85
|
+
(!this.ws || this.ws.readyState === WebSocket.CLOSED)) {
|
|
86
|
+
this.reconnectAttempts = 0
|
|
87
|
+
this.connect()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
document.addEventListener('visibilitychange', this.visibilityHandler)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Discriminated Union Message Types
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
type WebSocketMessage =
|
|
97
|
+
| { type: 'chunk'; content: string }
|
|
98
|
+
| { type: 'tool_call'; toolCall: ToolCall; persistedToolCallId?: string }
|
|
99
|
+
| { type: 'tool_result'; toolResult: ToolResult; persistedToolCallId?: string }
|
|
100
|
+
| { type: 'message'; message: Message }
|
|
101
|
+
| { type: 'messages_loaded'; messages: Message[]; hasMore: boolean }
|
|
102
|
+
| { type: 'connection_change'; connected: boolean }
|
|
103
|
+
| { type: 'complete' }
|
|
104
|
+
| { type: 'cancelled' }
|
|
105
|
+
| { type: 'error'; error: string }
|
|
106
|
+
| { type: 'generation_in_progress' }
|
|
107
|
+
| { type: 'token_limit_warning'; percentage: number; estimatedTokens: number; maxTokens: number }
|
|
108
|
+
| { type: 'progress_start'; toolCallId: string; command: string }
|
|
109
|
+
| { type: 'progress_update'; toolCallId: string; output: string }
|
|
110
|
+
| { type: 'progress_complete'; toolCallId: string; exitCode: number }
|
|
111
|
+
| { type: 'progress_error'; toolCallId: string; error: string }
|
|
112
|
+
| { type: 'status'; serverName?: string; statusMessage?: string }
|
|
113
|
+
| { type: 'conversation_created'; conversationId: string }
|
|
114
|
+
| { type: 'usage'; input_tokens: number; output_tokens: number }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Sending Messages
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
sendMessage(content: string | ContentBlock[], conversationId?: string) {
|
|
121
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
122
|
+
this.config.onMessage({ type: 'error', error: 'Not connected' })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
this.ws.send(JSON.stringify({
|
|
126
|
+
type: 'message', message: content,
|
|
127
|
+
userId: this.config.userId, conversationId, ghostOwner: this.config.ghostOwner,
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cancelGeneration(conversationId?: string) { /* send type: 'cancel' */ }
|
|
132
|
+
loadMessages(conversationId: string, limit?: number, startAfter?: string) { /* send type: 'load_messages' */ }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Graceful Disconnect
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
disconnect() {
|
|
139
|
+
this.intentionalDisconnect = true
|
|
140
|
+
clearTimeout(this.reconnectTimer)
|
|
141
|
+
document.removeEventListener('visibilitychange', this.visibilityHandler)
|
|
142
|
+
if (this.ws) {
|
|
143
|
+
this.ws.onclose = null; this.ws.onmessage = null; this.ws.onerror = null
|
|
144
|
+
this.ws.close()
|
|
145
|
+
this.ws = null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Component Integration
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Each ChatInterface creates its own WebSocket
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!user) return
|
|
156
|
+
const ws = new ChatWebSocket({
|
|
157
|
+
userId: user.uid,
|
|
158
|
+
conversationId,
|
|
159
|
+
ghostOwner,
|
|
160
|
+
onMessage: (msg) => {
|
|
161
|
+
switch (msg.type) {
|
|
162
|
+
case 'chunk': /* append to streaming blocks */ break
|
|
163
|
+
case 'message': /* add to message list */ break
|
|
164
|
+
case 'complete': /* finalize assistant message */ break
|
|
165
|
+
// ...
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
ws.connect()
|
|
170
|
+
wsClientRef.current = ws
|
|
171
|
+
return () => ws.disconnect()
|
|
172
|
+
}, [user, conversationId, ghostOwner])
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Server: Init Pre-Warming
|
|
176
|
+
|
|
177
|
+
On `init` message, the ChatRoom DO:
|
|
178
|
+
1. Registers user session
|
|
179
|
+
2. Loads last 50 messages from Firestore
|
|
180
|
+
3. Sends `messages_loaded` event
|
|
181
|
+
4. If active generation exists, sends `generation_in_progress`
|
|
182
|
+
5. Sends `ready` signal
|
|
183
|
+
|
|
184
|
+
### Server: ACL-Filtered Broadcasting
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
private broadcastMessage(message: ServerMessage, conversationId?: string) {
|
|
188
|
+
for (const [socket, userId] of this.sessions.entries()) {
|
|
189
|
+
if (conversationId && this.sessionConversations.get(socket) !== conversationId) continue
|
|
190
|
+
if (message.visible_to_user_ids && !message.visible_to_user_ids.includes(userId)) continue
|
|
191
|
+
socket.send(JSON.stringify(message))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Anti-Patterns
|
|
199
|
+
|
|
200
|
+
### Sharing a Single WebSocket Across Components
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// Bad: Pre-connected WebSocket has no-op handlers — messages get lost
|
|
204
|
+
const ws = useWebSocketManager() // Returns shared instance
|
|
205
|
+
<ChatInterface ws={ws} /> // Can't register proper handlers
|
|
206
|
+
|
|
207
|
+
// Good: Each component creates its own WebSocket
|
|
208
|
+
const ws = new ChatWebSocket({ userId, conversationId, onMessage: handleMessage })
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Not Clearing Handlers Before Closing
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Bad: Old onclose fires and triggers reconnect
|
|
215
|
+
this.ws.close()
|
|
216
|
+
|
|
217
|
+
// Good: Clear handlers first
|
|
218
|
+
this.ws.onclose = null; this.ws.onmessage = null; this.ws.onerror = null
|
|
219
|
+
this.ws.close()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Checklist
|
|
225
|
+
|
|
226
|
+
- [ ] Each component creates its own `ChatWebSocket` instance
|
|
227
|
+
- [ ] `onMessage` handler covers all discriminated union cases
|
|
228
|
+
- [ ] `disconnect()` called on component unmount
|
|
229
|
+
- [ ] Visibility handler registered for tab recovery
|
|
230
|
+
- [ ] Event handlers cleared before closing WebSocket
|
|
231
|
+
- [ ] Server broadcasts filter by conversationId + ACL visibility
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
**Status**: Stable
|
|
236
|
+
**Last Updated**: 2026-03-14
|
|
237
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# Wrangler Configuration Pattern
|
|
2
|
+
|
|
3
|
+
**Category**: Infrastructure
|
|
4
|
+
**Applicable To**: TanStack Start + Cloudflare Workers applications
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The `wrangler.toml` file configures how your TanStack Start application runs on Cloudflare Workers. This pattern documents the complete configuration required for a production TanStack Start application, including Durable Object bindings, rate limiting, migrations, observability, and environment management.
|
|
12
|
+
|
|
13
|
+
Getting the wrangler configuration right is essential — incorrect settings can cause deployment failures, missing bindings, or production runtime errors.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## When to Use This Pattern
|
|
18
|
+
|
|
19
|
+
✅ **Use this pattern when:**
|
|
20
|
+
- Deploying TanStack Start applications to Cloudflare Workers
|
|
21
|
+
- Using Durable Objects for stateful features
|
|
22
|
+
- Need rate limiting, observability, or environment variables
|
|
23
|
+
- Setting up production deployment configuration
|
|
24
|
+
|
|
25
|
+
❌ **Don't use this pattern when:**
|
|
26
|
+
- Not using Cloudflare Workers
|
|
27
|
+
- Using Cloudflare Pages instead of Workers
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Core Principles
|
|
32
|
+
|
|
33
|
+
1. **Single Configuration File**: All Worker configuration in `wrangler.toml`
|
|
34
|
+
2. **Explicit Bindings**: All Durable Objects, rate limiters, and KV namespaces explicitly declared
|
|
35
|
+
3. **Migration Tags**: Durable Object class changes tracked via sequential migration tags
|
|
36
|
+
4. **Secrets via CLI**: Sensitive values stored as Cloudflare secrets, never in wrangler.toml
|
|
37
|
+
5. **Compatibility Date**: Pin to a specific date for API stability
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Implementation
|
|
42
|
+
|
|
43
|
+
### Complete wrangler.toml Reference
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
# ─── Basic Configuration ─────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
name = "my-app"
|
|
49
|
+
main = "src/server.ts"
|
|
50
|
+
compatibility_date = "2026-02-10"
|
|
51
|
+
compatibility_flags = ["nodejs_compat"]
|
|
52
|
+
|
|
53
|
+
# ─── CPU Limits (Workers Paid) ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
[limits]
|
|
56
|
+
cpu_ms = 300000 # 5 minutes (300,000 ms) — maximum for Workers Paid plan
|
|
57
|
+
|
|
58
|
+
# ─── Observability ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
[observability]
|
|
61
|
+
enabled = true
|
|
62
|
+
|
|
63
|
+
[observability.logs]
|
|
64
|
+
enabled = true
|
|
65
|
+
invocation_logs = true
|
|
66
|
+
|
|
67
|
+
# ─── Durable Objects ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
[[durable_objects.bindings]]
|
|
70
|
+
name = "CHAT_ROOM"
|
|
71
|
+
class_name = "ChatRoom"
|
|
72
|
+
|
|
73
|
+
[[durable_objects.bindings]]
|
|
74
|
+
name = "UPLOAD_MANAGER"
|
|
75
|
+
class_name = "UploadManager"
|
|
76
|
+
|
|
77
|
+
# ─── Durable Object Migrations ───────────────────────────────────────────────
|
|
78
|
+
# Tags must be sequential. Each deployment reads these to determine schema changes.
|
|
79
|
+
|
|
80
|
+
[[migrations]]
|
|
81
|
+
tag = "v1"
|
|
82
|
+
new_sqlite_classes = ["ChatRoom"]
|
|
83
|
+
|
|
84
|
+
[[migrations]]
|
|
85
|
+
tag = "v2"
|
|
86
|
+
new_sqlite_classes = ["UploadManager"]
|
|
87
|
+
|
|
88
|
+
# To delete a Durable Object class:
|
|
89
|
+
# [[migrations]]
|
|
90
|
+
# tag = "v3"
|
|
91
|
+
# deleted_classes = ["ObsoleteClass"]
|
|
92
|
+
|
|
93
|
+
# ─── Rate Limiting ────────────────────────────────────────────────────────────
|
|
94
|
+
# namespace_id must be a string containing a positive integer
|
|
95
|
+
|
|
96
|
+
[[unsafe.bindings]]
|
|
97
|
+
name = "AUTH_RATE_LIMITER"
|
|
98
|
+
type = "ratelimit"
|
|
99
|
+
namespace_id = "1001"
|
|
100
|
+
simple = { limit = 5, period = 60 }
|
|
101
|
+
|
|
102
|
+
[[unsafe.bindings]]
|
|
103
|
+
name = "API_RATE_LIMITER"
|
|
104
|
+
type = "ratelimit"
|
|
105
|
+
namespace_id = "1002"
|
|
106
|
+
simple = { limit = 100, period = 60 }
|
|
107
|
+
|
|
108
|
+
[[unsafe.bindings]]
|
|
109
|
+
name = "WS_RATE_LIMITER"
|
|
110
|
+
type = "ratelimit"
|
|
111
|
+
namespace_id = "1003"
|
|
112
|
+
simple = { limit = 10, period = 60 }
|
|
113
|
+
|
|
114
|
+
# ─── Environment Variables (non-secret) ──────────────────────────────────────
|
|
115
|
+
# [vars]
|
|
116
|
+
# APP_ENV = "production"
|
|
117
|
+
# LOG_LEVEL = "info"
|
|
118
|
+
|
|
119
|
+
# ─── Secrets (set via CLI, never in this file) ───────────────────────────────
|
|
120
|
+
# wrangler secret put AWS_ACCESS_KEY_ID
|
|
121
|
+
# wrangler secret put AWS_SECRET_ACCESS_KEY
|
|
122
|
+
# wrangler secret put FIREBASE_PROJECT_ID
|
|
123
|
+
# wrangler secret put FIREBASE_CLIENT_EMAIL
|
|
124
|
+
# wrangler secret put FIREBASE_PRIVATE_KEY
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Vite Configuration
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// vite.config.ts
|
|
131
|
+
import { defineConfig } from 'vite'
|
|
132
|
+
import { cloudflare } from '@opennextjs/cloudflare'
|
|
133
|
+
import { TanStackStartVite as tanstackStart } from '@tanstack/start/vite-plugin'
|
|
134
|
+
import react from '@vitejs/plugin-react'
|
|
135
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
136
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths'
|
|
137
|
+
|
|
138
|
+
export default defineConfig({
|
|
139
|
+
plugins: [
|
|
140
|
+
cloudflare({ viteEnvironment: { name: 'ssr' } }),
|
|
141
|
+
viteTsConfigPaths(),
|
|
142
|
+
tailwindcss(),
|
|
143
|
+
tanstackStart(),
|
|
144
|
+
react(),
|
|
145
|
+
],
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### TypeScript Env Types
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// src/env.d.ts (or generated via wrangler types)
|
|
153
|
+
interface Env {
|
|
154
|
+
// Durable Objects
|
|
155
|
+
CHAT_ROOM: DurableObjectNamespace
|
|
156
|
+
UPLOAD_MANAGER: DurableObjectNamespace
|
|
157
|
+
|
|
158
|
+
// Rate Limiters
|
|
159
|
+
AUTH_RATE_LIMITER: RateLimit
|
|
160
|
+
API_RATE_LIMITER: RateLimit
|
|
161
|
+
WS_RATE_LIMITER: RateLimit
|
|
162
|
+
|
|
163
|
+
// Secrets
|
|
164
|
+
AWS_ACCESS_KEY_ID: string
|
|
165
|
+
AWS_SECRET_ACCESS_KEY: string
|
|
166
|
+
FIREBASE_PROJECT_ID: string
|
|
167
|
+
FIREBASE_CLIENT_EMAIL: string
|
|
168
|
+
FIREBASE_PRIVATE_KEY: string
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Configuration Sections Explained
|
|
175
|
+
|
|
176
|
+
### Basic Configuration
|
|
177
|
+
|
|
178
|
+
| Field | Purpose | Example |
|
|
179
|
+
|-------|---------|---------|
|
|
180
|
+
| `name` | Worker name (used in deployment) | `"my-app"` |
|
|
181
|
+
| `main` | Entry point file | `"src/server.ts"` |
|
|
182
|
+
| `compatibility_date` | Pin Cloudflare API version | `"2026-02-10"` |
|
|
183
|
+
| `compatibility_flags` | Enable Node.js APIs | `["nodejs_compat"]` |
|
|
184
|
+
|
|
185
|
+
### CPU Limits
|
|
186
|
+
|
|
187
|
+
| Field | Purpose | Default | Paid Plan Max |
|
|
188
|
+
|-------|---------|---------|---------------|
|
|
189
|
+
| `cpu_ms` | Max CPU time per request | 10ms (free) | 300,000ms (paid) |
|
|
190
|
+
|
|
191
|
+
### Durable Object Bindings
|
|
192
|
+
|
|
193
|
+
Each binding creates a `DurableObjectNamespace` accessible via `env.BINDING_NAME`:
|
|
194
|
+
- `name`: The binding name in your code (e.g., `env.CHAT_ROOM`)
|
|
195
|
+
- `class_name`: The exported class name in your source code
|
|
196
|
+
|
|
197
|
+
### Migrations
|
|
198
|
+
|
|
199
|
+
Migrations track Durable Object schema changes:
|
|
200
|
+
- `new_sqlite_classes`: New DO classes that use SQLite storage
|
|
201
|
+
- `new_classes`: New DO classes (without SQLite)
|
|
202
|
+
- `deleted_classes`: Classes being removed
|
|
203
|
+
- Tags must be sequential and never reused
|
|
204
|
+
|
|
205
|
+
### Secrets Management
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Upload individual secrets
|
|
209
|
+
wrangler secret put FIREBASE_PRIVATE_KEY
|
|
210
|
+
|
|
211
|
+
# Upload from .env file (script)
|
|
212
|
+
while IFS='=' read -r key value; do
|
|
213
|
+
echo "$value" | wrangler secret put "$key"
|
|
214
|
+
done < .env.secrets
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Examples
|
|
220
|
+
|
|
221
|
+
### Example 1: Adding a New Durable Object
|
|
222
|
+
|
|
223
|
+
```toml
|
|
224
|
+
# 1. Add binding
|
|
225
|
+
[[durable_objects.bindings]]
|
|
226
|
+
name = "TASK_EXECUTOR"
|
|
227
|
+
class_name = "TaskExecutor"
|
|
228
|
+
|
|
229
|
+
# 2. Add migration (use next sequential tag)
|
|
230
|
+
[[migrations]]
|
|
231
|
+
tag = "v3"
|
|
232
|
+
new_sqlite_classes = ["TaskExecutor"]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Example 2: Removing a Durable Object
|
|
236
|
+
|
|
237
|
+
```toml
|
|
238
|
+
# Migration to delete — add this, don't remove the binding yet
|
|
239
|
+
[[migrations]]
|
|
240
|
+
tag = "v4"
|
|
241
|
+
deleted_classes = ["TaskExecutor"]
|
|
242
|
+
|
|
243
|
+
# After deploying, remove the binding:
|
|
244
|
+
# [[durable_objects.bindings]]
|
|
245
|
+
# name = "TASK_EXECUTOR"
|
|
246
|
+
# class_name = "TaskExecutor"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Example 3: Custom Domain Route
|
|
250
|
+
|
|
251
|
+
```toml
|
|
252
|
+
# Route to custom domain
|
|
253
|
+
[[routes]]
|
|
254
|
+
pattern = "app.example.com/*"
|
|
255
|
+
zone_name = "example.com"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Benefits
|
|
261
|
+
|
|
262
|
+
### 1. Declarative Configuration
|
|
263
|
+
All infrastructure described in a single, version-controlled file.
|
|
264
|
+
|
|
265
|
+
### 2. Type-Safe Bindings
|
|
266
|
+
Generate TypeScript types with `wrangler types` for full type safety.
|
|
267
|
+
|
|
268
|
+
### 3. Sequential Migrations
|
|
269
|
+
Durable Object schema changes are tracked and applied in order.
|
|
270
|
+
|
|
271
|
+
### 4. Edge Observability
|
|
272
|
+
Built-in logging and invocation tracking without external services.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Trade-offs
|
|
277
|
+
|
|
278
|
+
### 1. Rate Limiting Uses unsafe.bindings
|
|
279
|
+
**Downside**: The `unsafe.bindings` API may change in future Cloudflare versions.
|
|
280
|
+
**Mitigation**: Abstract rate limiting behind utility functions for easy migration.
|
|
281
|
+
|
|
282
|
+
### 2. No Environment Inheritance
|
|
283
|
+
**Downside**: No built-in way to share config between staging and production.
|
|
284
|
+
**Mitigation**: Use separate wrangler.toml files or environment sections.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Anti-Patterns
|
|
289
|
+
|
|
290
|
+
### ❌ Anti-Pattern 1: Secrets in wrangler.toml
|
|
291
|
+
|
|
292
|
+
```toml
|
|
293
|
+
# ❌ BAD: Secrets in config file (committed to git!)
|
|
294
|
+
[vars]
|
|
295
|
+
FIREBASE_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----..."
|
|
296
|
+
|
|
297
|
+
# ✅ GOOD: Use wrangler secret
|
|
298
|
+
# wrangler secret put FIREBASE_PRIVATE_KEY
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### ❌ Anti-Pattern 2: Reusing Migration Tags
|
|
302
|
+
|
|
303
|
+
```toml
|
|
304
|
+
# ❌ BAD: Reusing tag "v1" after changing it
|
|
305
|
+
[[migrations]]
|
|
306
|
+
tag = "v1"
|
|
307
|
+
new_sqlite_classes = ["ChatRoom", "UploadManager"] # Changed!
|
|
308
|
+
|
|
309
|
+
# ✅ GOOD: Sequential tags, never modified
|
|
310
|
+
[[migrations]]
|
|
311
|
+
tag = "v1"
|
|
312
|
+
new_sqlite_classes = ["ChatRoom"]
|
|
313
|
+
|
|
314
|
+
[[migrations]]
|
|
315
|
+
tag = "v2"
|
|
316
|
+
new_sqlite_classes = ["UploadManager"]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### ❌ Anti-Pattern 3: Missing nodejs_compat
|
|
320
|
+
|
|
321
|
+
```toml
|
|
322
|
+
# ❌ BAD: Missing Node.js compatibility (crypto, Buffer, etc. won't work)
|
|
323
|
+
compatibility_flags = []
|
|
324
|
+
|
|
325
|
+
# ✅ GOOD: Enable Node.js compatibility
|
|
326
|
+
compatibility_flags = ["nodejs_compat"]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Related Patterns
|
|
332
|
+
|
|
333
|
+
- **[Durable Objects WebSocket](./tanstack-cloudflare.durable-objects-websocket.md)**: DO bindings and migrations
|
|
334
|
+
- **[Rate Limiting](./tanstack-cloudflare.rate-limiting.md)**: Rate limiter bindings
|
|
335
|
+
- **[Auth Session Management](./tanstack-cloudflare.auth-session-management.md)**: Secret management for auth keys
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Checklist for Implementation
|
|
340
|
+
|
|
341
|
+
- [ ] `name` set to your worker name
|
|
342
|
+
- [ ] `main` points to server entry point
|
|
343
|
+
- [ ] `compatibility_date` set to recent date
|
|
344
|
+
- [ ] `nodejs_compat` flag enabled
|
|
345
|
+
- [ ] CPU limits configured for paid plan
|
|
346
|
+
- [ ] Observability and invocation logs enabled
|
|
347
|
+
- [ ] All Durable Objects have bindings
|
|
348
|
+
- [ ] Migrations use sequential, never-reused tags
|
|
349
|
+
- [ ] Rate limiters configured per endpoint category
|
|
350
|
+
- [ ] Secrets stored via `wrangler secret`, never in config
|
|
351
|
+
- [ ] Env types generated or manually maintained
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
**Status**: Stable - Essential configuration for Cloudflare Workers deployment
|
|
356
|
+
**Recommendation**: Review and customize for every new TanStack Start + Cloudflare project
|
|
357
|
+
**Last Updated**: 2026-02-28
|
|
358
|
+
**Contributors**: Patrick Michaelsen
|