@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,461 @@
|
|
|
1
|
+
# Next.js to TanStack Start Routing Migration
|
|
2
|
+
|
|
3
|
+
**Category**: Migration
|
|
4
|
+
**Applicable To**: Projects migrating from Next.js App Router to TanStack Start + Cloudflare Workers
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This pattern maps Next.js App Router conventions to their TanStack Start equivalents. It covers page routes, layouts, API routes, dynamic parameters, metadata, server-side data fetching, and middleware. The goal is a systematic migration where each Next.js file has a clear TanStack Start counterpart.
|
|
12
|
+
|
|
13
|
+
Both frameworks use file-based routing, but they differ in naming conventions, data loading strategies, and API route syntax. This guide provides side-by-side mappings with code examples.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## When to Use This Pattern
|
|
18
|
+
|
|
19
|
+
✅ **Use this pattern when:**
|
|
20
|
+
- Migrating an existing Next.js App Router application to TanStack Start
|
|
21
|
+
- Need a reference for mapping Next.js conventions to TanStack equivalents
|
|
22
|
+
- Building new features and want to know the TanStack way of doing something familiar from Next.js
|
|
23
|
+
|
|
24
|
+
❌ **Don't use this pattern when:**
|
|
25
|
+
- Migrating from Next.js Pages Router (different conventions)
|
|
26
|
+
- Building a new project from scratch (use other patterns directly)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Route File Mapping
|
|
31
|
+
|
|
32
|
+
| Next.js App Router | TanStack Start | Notes |
|
|
33
|
+
|-------------------|----------------|-------|
|
|
34
|
+
| `app/page.tsx` | `routes/index.tsx` | Home page |
|
|
35
|
+
| `app/layout.tsx` | `routes/__root.tsx` | Root layout |
|
|
36
|
+
| `app/about/page.tsx` | `routes/about.tsx` | Static page |
|
|
37
|
+
| `app/blog/[id]/page.tsx` | `routes/blog/$id.tsx` | Dynamic route (`[id]` → `$id`) |
|
|
38
|
+
| `app/blog/[...slug]/page.tsx` | `routes/blog/$.tsx` | Catch-all route |
|
|
39
|
+
| `app/api/posts/route.ts` | `routes/api/posts/index.tsx` | API route |
|
|
40
|
+
| `app/api/posts/[id]/route.ts` | `routes/api/posts/$id.tsx` | Dynamic API route |
|
|
41
|
+
| `app/(group)/page.tsx` | `routes/_group/index.tsx` | Route group (parentheses → underscore) |
|
|
42
|
+
| `app/loading.tsx` | `pendingComponent` on route | Loading state |
|
|
43
|
+
| `app/error.tsx` | `errorComponent` on route | Error boundary |
|
|
44
|
+
| `app/not-found.tsx` | `notFoundComponent` on route | 404 handler |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Page Routes
|
|
49
|
+
|
|
50
|
+
### Next.js: Server Component with Data Fetching
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// app/home/page.tsx (Next.js)
|
|
54
|
+
import { cookies } from 'next/headers'
|
|
55
|
+
import { getServerSession } from '@/lib/auth-server'
|
|
56
|
+
import HomePageClient from './HomePageClient'
|
|
57
|
+
|
|
58
|
+
export const metadata = {
|
|
59
|
+
title: 'Home',
|
|
60
|
+
description: 'Your neighborhood feed',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function HomePage() {
|
|
64
|
+
const cookieStore = cookies()
|
|
65
|
+
const session = cookieStore.get('session')
|
|
66
|
+
// Fetch data server-side...
|
|
67
|
+
return <HomePageClient />
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### TanStack Start: Route with beforeLoad
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// routes/home.tsx (TanStack Start)
|
|
75
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
76
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
77
|
+
|
|
78
|
+
export const Route = createFileRoute('/home')({
|
|
79
|
+
// Server-side data fetching (replaces async Server Component)
|
|
80
|
+
beforeLoad: async () => {
|
|
81
|
+
const user = await getAuthSession()
|
|
82
|
+
if (!user) {
|
|
83
|
+
throw redirect({ to: '/auth', search: { redirect_url: '/home' } })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let posts = []
|
|
87
|
+
try {
|
|
88
|
+
posts = await PostDatabaseService.getFeed(user.uid, 50)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Failed to preload feed:', error)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { user, posts }
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// SEO metadata (replaces export const metadata)
|
|
97
|
+
meta: () => [
|
|
98
|
+
{ title: 'Home' },
|
|
99
|
+
{ name: 'description', content: 'Your neighborhood feed' },
|
|
100
|
+
],
|
|
101
|
+
|
|
102
|
+
component: HomePage,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
function HomePage() {
|
|
106
|
+
const { user, posts } = Route.useRouteContext()
|
|
107
|
+
return <HomePageClient initialPosts={posts} />
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Layouts
|
|
114
|
+
|
|
115
|
+
### Next.js: layout.tsx
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// app/layout.tsx (Next.js)
|
|
119
|
+
import { cookies } from 'next/headers'
|
|
120
|
+
import ReduxProvider from '@/components/ReduxProvider'
|
|
121
|
+
|
|
122
|
+
export default async function RootLayout({ children }) {
|
|
123
|
+
const cookieStore = cookies()
|
|
124
|
+
const sessionCookie = cookieStore.get('session')
|
|
125
|
+
|
|
126
|
+
let initialState
|
|
127
|
+
try {
|
|
128
|
+
initialState = await getStateFromHeaders(sessionCookie?.value)
|
|
129
|
+
} catch { initialState = {} }
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<body>
|
|
134
|
+
<ReduxProvider initialState={initialState}>
|
|
135
|
+
<Navbar />
|
|
136
|
+
{children}
|
|
137
|
+
<ModalContainer />
|
|
138
|
+
<ToastContainer />
|
|
139
|
+
</ReduxProvider>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### TanStack Start: __root.tsx
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// routes/__root.tsx (TanStack Start)
|
|
150
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
|
|
151
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
152
|
+
import ReduxProvider from '@/components/ReduxProvider'
|
|
153
|
+
|
|
154
|
+
export const Route = createRootRouteWithContext()({
|
|
155
|
+
beforeLoad: async () => {
|
|
156
|
+
const user = await getAuthSession()
|
|
157
|
+
return { user }
|
|
158
|
+
},
|
|
159
|
+
component: RootLayout,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
function RootLayout() {
|
|
163
|
+
const { user } = Route.useRouteContext()
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<ReduxProvider initialUser={user}>
|
|
167
|
+
<Navbar />
|
|
168
|
+
<Outlet /> {/* Replaces {children} */}
|
|
169
|
+
<ModalContainer />
|
|
170
|
+
<ToastContainer />
|
|
171
|
+
</ReduxProvider>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Key differences**:
|
|
177
|
+
- `{children}` → `<Outlet />`
|
|
178
|
+
- `cookies()` from `next/headers` → `getAuthSession()` server function
|
|
179
|
+
- Metadata in `export const metadata` → `meta` function on route
|
|
180
|
+
- `<html>` and `<body>` go in a separate entry file, not in `__root.tsx`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## API Routes
|
|
185
|
+
|
|
186
|
+
### Next.js: Route Handlers
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// app/api/posts/create/route.ts (Next.js)
|
|
190
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
191
|
+
import { getServerSession } from '@/lib/auth-server'
|
|
192
|
+
|
|
193
|
+
export async function POST(request: NextRequest) {
|
|
194
|
+
const session = await getServerSession(request)
|
|
195
|
+
if (!session?.user) {
|
|
196
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const body = await request.json()
|
|
200
|
+
const post = await PostDatabaseService.create(session.user.uid, body)
|
|
201
|
+
|
|
202
|
+
return NextResponse.json(post, { status: 201 })
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### TanStack Start: Server Handlers
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// routes/api/posts/create.tsx (TanStack Start)
|
|
210
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
211
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
212
|
+
|
|
213
|
+
export const Route = createFileRoute('/api/posts/create')({
|
|
214
|
+
server: {
|
|
215
|
+
handlers: {
|
|
216
|
+
POST: async ({ request }) => {
|
|
217
|
+
const user = await getAuthSession()
|
|
218
|
+
if (!user) {
|
|
219
|
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
220
|
+
status: 401,
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const body = await request.json()
|
|
226
|
+
const post = await PostDatabaseService.create(user.uid, body)
|
|
227
|
+
|
|
228
|
+
return new Response(JSON.stringify(post), {
|
|
229
|
+
status: 201,
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
})
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Key differences**:
|
|
239
|
+
- `NextResponse.json()` → `new Response(JSON.stringify())` with headers
|
|
240
|
+
- `export async function POST` → `server.handlers.POST`
|
|
241
|
+
- `NextRequest` type → standard `Request`
|
|
242
|
+
- File extension `.ts` → `.tsx`
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Dynamic Routes
|
|
247
|
+
|
|
248
|
+
### Next.js: [id] Parameter
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// app/api/posts/[id]/route.ts (Next.js)
|
|
252
|
+
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
|
253
|
+
const post = await PostDatabaseService.getById(params.id)
|
|
254
|
+
return NextResponse.json(post)
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### TanStack Start: $id Parameter
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// routes/api/posts/$id.tsx (TanStack Start)
|
|
262
|
+
export const Route = createFileRoute('/api/posts/$id')({
|
|
263
|
+
server: {
|
|
264
|
+
handlers: {
|
|
265
|
+
GET: async ({ params }) => {
|
|
266
|
+
const post = await PostDatabaseService.getById(params.id)
|
|
267
|
+
return new Response(JSON.stringify(post), {
|
|
268
|
+
status: 200,
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
})
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Key difference**: `[id]` → `$id` in file names. Params accessed the same way.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Server-Side Data Fetching
|
|
282
|
+
|
|
283
|
+
| Next.js Pattern | TanStack Start Equivalent |
|
|
284
|
+
|----------------|--------------------------|
|
|
285
|
+
| `async function Page()` (Server Component) | `beforeLoad` on route |
|
|
286
|
+
| `cookies()` from `next/headers` | `getRequest()` from `@tanstack/react-start/server` |
|
|
287
|
+
| `headers()` from `next/headers` | `getRequest().headers` |
|
|
288
|
+
| `fetch()` in Server Component | Database service call in `beforeLoad` |
|
|
289
|
+
| `revalidatePath()` / `revalidateTag()` | Not needed (no ISR — always fresh on Workers) |
|
|
290
|
+
| `generateStaticParams()` | Not applicable (no SSG on Workers) |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Metadata / SEO
|
|
295
|
+
|
|
296
|
+
### Next.js: Metadata Export
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// app/profile/[username]/page.tsx (Next.js)
|
|
300
|
+
export async function generateMetadata({ params }) {
|
|
301
|
+
const profile = await getProfile(params.username)
|
|
302
|
+
return {
|
|
303
|
+
title: profile.displayName,
|
|
304
|
+
openGraph: { title: profile.displayName, images: [profile.avatar] },
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### TanStack Start: meta Function
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// routes/profile/$username.tsx (TanStack Start)
|
|
313
|
+
export const Route = createFileRoute('/profile/$username')({
|
|
314
|
+
beforeLoad: async ({ params }) => {
|
|
315
|
+
const profile = await ProfileDatabaseService.getByUsername(params.username)
|
|
316
|
+
return { profile }
|
|
317
|
+
},
|
|
318
|
+
meta: ({ loaderData }) => [
|
|
319
|
+
{ title: loaderData.profile?.displayName },
|
|
320
|
+
{ property: 'og:title', content: loaderData.profile?.displayName },
|
|
321
|
+
],
|
|
322
|
+
component: ProfilePage,
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Middleware
|
|
329
|
+
|
|
330
|
+
### Next.js: middleware.ts
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// middleware.ts (Next.js — runs on every request)
|
|
334
|
+
import { NextResponse } from 'next/server'
|
|
335
|
+
export function middleware(request) {
|
|
336
|
+
if (request.nextUrl.pathname.startsWith('/admin')) {
|
|
337
|
+
// Check auth, redirect if needed
|
|
338
|
+
}
|
|
339
|
+
return NextResponse.next()
|
|
340
|
+
}
|
|
341
|
+
export const config = { matcher: ['/admin/:path*'] }
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### TanStack Start: beforeLoad on Parent Route
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// routes/admin.tsx (TanStack Start — layout route for /admin/*)
|
|
348
|
+
export const Route = createFileRoute('/admin')({
|
|
349
|
+
beforeLoad: async () => {
|
|
350
|
+
const user = await getAuthSession()
|
|
351
|
+
if (!user?.isAdmin) {
|
|
352
|
+
throw redirect({ to: '/auth' })
|
|
353
|
+
}
|
|
354
|
+
return { user }
|
|
355
|
+
},
|
|
356
|
+
component: AdminLayout,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
function AdminLayout() {
|
|
360
|
+
return <Outlet /> // Child admin routes render here
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Key difference**: No global middleware file. Use `beforeLoad` on parent routes for path-specific guards.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## SSE / Streaming Responses
|
|
369
|
+
|
|
370
|
+
### Next.js: ReadableStream
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// app/api/chat/stream/route.ts (Next.js)
|
|
374
|
+
export async function POST(request: NextRequest) {
|
|
375
|
+
const stream = new ReadableStream({
|
|
376
|
+
async start(controller) {
|
|
377
|
+
const encoder = new TextEncoder()
|
|
378
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'chunk' })}\n\n`))
|
|
379
|
+
controller.close()
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
return new Response(stream, {
|
|
383
|
+
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### TanStack Start: Same Pattern (Web Standards)
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// routes/api/chat/stream.tsx (TanStack Start)
|
|
392
|
+
POST: async ({ request }) => {
|
|
393
|
+
const stream = new ReadableStream({
|
|
394
|
+
async start(controller) {
|
|
395
|
+
const encoder = new TextEncoder()
|
|
396
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'chunk' })}\n\n`))
|
|
397
|
+
controller.close()
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
return new Response(stream, {
|
|
401
|
+
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
SSE streaming is identical — both use Web Standard `ReadableStream`. However, for real-time chat consider migrating to **Durable Objects WebSocket** instead of SSE.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Migration Checklist
|
|
411
|
+
|
|
412
|
+
### Phase 1: Project Setup
|
|
413
|
+
- [ ] Initialize TanStack Start project with Cloudflare Workers
|
|
414
|
+
- [ ] Configure `wrangler.toml` (see [wrangler-configuration](./tanstack-cloudflare.wrangler-configuration.md))
|
|
415
|
+
- [ ] Set up `vite.config.ts` with TanStack + Cloudflare plugins
|
|
416
|
+
- [ ] Configure path aliases (`@/` → `./src/`)
|
|
417
|
+
- [ ] Move environment variables to Cloudflare secrets
|
|
418
|
+
|
|
419
|
+
### Phase 2: Route Migration
|
|
420
|
+
- [ ] Convert `app/layout.tsx` → `routes/__root.tsx`
|
|
421
|
+
- [ ] Convert `app/page.tsx` → `routes/index.tsx`
|
|
422
|
+
- [ ] Convert page routes: `app/X/page.tsx` → `routes/X.tsx`
|
|
423
|
+
- [ ] Convert dynamic routes: `[id]` → `$id`
|
|
424
|
+
- [ ] Convert `metadata` exports → `meta` functions
|
|
425
|
+
- [ ] Move `{children}` to `<Outlet />`
|
|
426
|
+
|
|
427
|
+
### Phase 3: API Route Migration
|
|
428
|
+
- [ ] Convert `app/api/X/route.ts` → `routes/api/X/index.tsx`
|
|
429
|
+
- [ ] Replace `NextRequest`/`NextResponse` with Web Standard `Request`/`Response`
|
|
430
|
+
- [ ] Replace `export async function GET/POST` → `server.handlers.GET/POST`
|
|
431
|
+
- [ ] Replace `cookies()` API with cookie parsing from request headers
|
|
432
|
+
|
|
433
|
+
### Phase 4: Data Fetching Migration
|
|
434
|
+
- [ ] Replace async Server Components with `beforeLoad`
|
|
435
|
+
- [ ] Replace `cookies()`/`headers()` with `getAuthSession()` server function
|
|
436
|
+
- [ ] Remove `revalidatePath`/`revalidateTag` (not needed on Workers)
|
|
437
|
+
- [ ] Remove `generateStaticParams` (no SSG on Workers)
|
|
438
|
+
|
|
439
|
+
### Phase 5: Next.js Specific Removal
|
|
440
|
+
- [ ] Remove `next.config.js`
|
|
441
|
+
- [ ] Remove `middleware.ts` (use `beforeLoad` guards)
|
|
442
|
+
- [ ] Remove `next/image` usage (use standard `<img>` or Cloudflare Images)
|
|
443
|
+
- [ ] Remove `next/link` (use TanStack Router `<Link>`)
|
|
444
|
+
- [ ] Remove `next/navigation` (use TanStack Router hooks)
|
|
445
|
+
- [ ] Remove `next/headers` usage
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Related Patterns
|
|
450
|
+
|
|
451
|
+
- **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Target API route pattern
|
|
452
|
+
- **[SSR Preload](./tanstack-cloudflare.ssr-preload.md)**: Replaces async Server Components
|
|
453
|
+
- **[Auth Session Management](./tanstack-cloudflare.auth-session-management.md)**: Replaces `cookies()` auth pattern
|
|
454
|
+
- **[Wrangler Configuration](./tanstack-cloudflare.wrangler-configuration.md)**: Replaces `vercel.json` + `next.config.js`
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
**Status**: Stable - Comprehensive migration reference
|
|
459
|
+
**Recommendation**: Use as a lookup guide during Next.js → TanStack Start migration
|
|
460
|
+
**Last Updated**: 2026-02-28
|
|
461
|
+
**Contributors**: Patrick Michaelsen
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Notifications Engine
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: Real-time push notifications via WebSocket with multi-tab sync, exponential backoff, and FCM fallback for offline users
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A three-layer notification system: NotificationsEngine (client WebSocket with event subscriptions), NotificationHub (per-user Durable Object broadcasting to all connected tabs), and NotificationTriggers (server-side delivery with WebSocket-first, FCM-fallback strategy). Notifications flow in real-time to all open tabs; when the user is offline, FCM push notifications deliver instead.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### NotificationsEngine (Client)
|
|
18
|
+
|
|
19
|
+
**File**: `src/lib/notifications/notifications-engine.ts`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
type NotificationEventType = 'notification' | 'notification_read' | 'notification_removed'
|
|
23
|
+
| 'unread_count' | 'connection_change'
|
|
24
|
+
|
|
25
|
+
class NotificationsEngine {
|
|
26
|
+
private ws: WebSocket | null = null
|
|
27
|
+
private handlers: Map<NotificationEventType, Set<EventHandler>> = new Map()
|
|
28
|
+
private reconnectAttempts = 0
|
|
29
|
+
private maxReconnectAttempts = 10
|
|
30
|
+
private reconnectDelay = 1000
|
|
31
|
+
private intentionalClose = false
|
|
32
|
+
|
|
33
|
+
connect() {
|
|
34
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
35
|
+
this.ws = new WebSocket(`${protocol}//${location.host}/api/notifications-ws`)
|
|
36
|
+
this.ws.onopen = () => { this.reconnectAttempts = 0; this.emit('connection_change', { connected: true }) }
|
|
37
|
+
this.ws.onmessage = (e) => { const data = JSON.parse(e.data); this.emit(data.type, data) }
|
|
38
|
+
this.ws.onclose = () => { this.emit('connection_change', { connected: false }); this.attemptReconnect() }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
on<T extends NotificationEventType>(event: T, handler: EventHandler<T>): () => void {
|
|
42
|
+
if (!this.handlers.has(event)) this.handlers.set(event, new Set())
|
|
43
|
+
this.handlers.get(event)!.add(handler)
|
|
44
|
+
return () => { this.handlers.get(event)?.delete(handler) } // Unsubscribe
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnect() { this.intentionalClose = true; this.ws?.close() }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Backoff: `1000 * 2^(attempt-1)` — 1s, 2s, 4s, ... 512s, then give up after 10 attempts.
|
|
52
|
+
|
|
53
|
+
### NotificationHub (Server — Durable Object)
|
|
54
|
+
|
|
55
|
+
**File**: `src/durable-objects/NotificationHub.ts`
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
class NotificationHub extends DurableObject {
|
|
59
|
+
private sessions: Set<WebSocket> = new Set()
|
|
60
|
+
|
|
61
|
+
async fetch(request: Request) {
|
|
62
|
+
if (url.pathname === '/broadcast') {
|
|
63
|
+
const event = await request.json()
|
|
64
|
+
this.broadcast(event) // Send to all connected tabs
|
|
65
|
+
return new Response('ok')
|
|
66
|
+
}
|
|
67
|
+
if (url.pathname === '/connected') {
|
|
68
|
+
return Response.json({ connected: this.sessions.size > 0, count: this.sessions.size })
|
|
69
|
+
}
|
|
70
|
+
// WebSocket upgrade
|
|
71
|
+
const [client, server] = Object.values(new WebSocketPair())
|
|
72
|
+
this.ctx.acceptWebSocket(server)
|
|
73
|
+
this.sessions.add(server)
|
|
74
|
+
return new Response(null, { status: 101, webSocket: client })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private broadcast(event: NotificationEvent) {
|
|
78
|
+
for (const ws of this.sessions) {
|
|
79
|
+
try { ws.send(JSON.stringify(event)) }
|
|
80
|
+
catch { this.sessions.delete(ws) } // Clean dead connections
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
One DO instance per user (`idFromName(userId)`). Push-only channel — clients don't send messages.
|
|
87
|
+
|
|
88
|
+
### NotificationTriggers (Delivery Strategy)
|
|
89
|
+
|
|
90
|
+
**File**: `src/services/notification-triggers.service.ts`
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
private static async deliver(recipientId, notification, pushData?, env?) {
|
|
94
|
+
if (env) {
|
|
95
|
+
const connected = await NotificationHubService.isUserConnected(env, recipientId)
|
|
96
|
+
if (connected) {
|
|
97
|
+
// In-app: WebSocket only (updates bell badge instantly)
|
|
98
|
+
await NotificationHubService.pushNotification(env, recipientId, notification)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Offline: FCM push notification
|
|
103
|
+
await FcmService.sendToUser(recipientId, { title: notification.title, body: notification.message })
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Multi-Tab Sync
|
|
108
|
+
|
|
109
|
+
When any tab marks a notification as read:
|
|
110
|
+
1. API updates Firestore
|
|
111
|
+
2. API broadcasts `notification_read` to NotificationHub
|
|
112
|
+
3. Hub sends to ALL connected tabs
|
|
113
|
+
4. Each tab's `engine.on('notification_read')` decrements unread count
|
|
114
|
+
|
|
115
|
+
### Component Integration
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
function NotificationBell() {
|
|
119
|
+
const [unreadCount, setUnreadCount] = useState(0)
|
|
120
|
+
const engineRef = useRef<NotificationsEngine | null>(null)
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!user?.uid) return
|
|
124
|
+
const engine = new NotificationsEngine(user.uid)
|
|
125
|
+
|
|
126
|
+
engine.on('notification', () => setUnreadCount(c => c + 1))
|
|
127
|
+
engine.on('notification_read', () => setUnreadCount(c => Math.max(0, c - 1)))
|
|
128
|
+
engine.on('notification_removed', () => refetchCount())
|
|
129
|
+
|
|
130
|
+
engine.connect()
|
|
131
|
+
engineRef.current = engine
|
|
132
|
+
return () => engine.disconnect()
|
|
133
|
+
}, [user?.uid])
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Checklist
|
|
140
|
+
|
|
141
|
+
- [ ] One NotificationHub DO per user (`idFromName(userId)`)
|
|
142
|
+
- [ ] Engine `on()` returns unsubscribe function — call it on unmount
|
|
143
|
+
- [ ] Delivery checks WebSocket connectivity first, falls back to FCM
|
|
144
|
+
- [ ] API endpoints broadcast changes for multi-tab sync
|
|
145
|
+
- [ ] Push-only WebSocket — clients receive, never send
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**Status**: Stable
|
|
150
|
+
**Last Updated**: 2026-03-14
|
|
151
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# OAuth Token Refresh Queue
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: Proactive OAuth credential rotation with cron-driven queue processing and per-provider refresh logic
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A Firestore-backed queue system for proactive OAuth token refresh. After OAuth callback, credentials are enqueued with a `next_refresh_at` timestamp (e.g., 10 minutes before expiry). A cron job queries the queue for expiring entries, performs per-provider token rotation (Google refresh_token exchange, Instagram stateless refresh), updates credentials, and re-enqueues with the next refresh time.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
**File**: `src/services/oauth-refresh.service.ts`
|
|
18
|
+
|
|
19
|
+
### Queue Entry
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface RefreshQueueEntry {
|
|
23
|
+
user_id: string
|
|
24
|
+
provider: string
|
|
25
|
+
next_refresh_at: string // ISO timestamp
|
|
26
|
+
created_at: string
|
|
27
|
+
updated_at: string
|
|
28
|
+
}
|
|
29
|
+
// Collection: oauth-refresh-queue
|
|
30
|
+
// Document ID: {userId}_{provider}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Service Methods
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
class OAuthRefreshService {
|
|
37
|
+
// Called after OAuth callback — schedule first refresh
|
|
38
|
+
static async enqueueRefresh(userId, provider, nextRefreshAt): Promise<void>
|
|
39
|
+
|
|
40
|
+
// Called on disconnect — remove from queue
|
|
41
|
+
static async dequeueRefresh(userId, provider): Promise<void>
|
|
42
|
+
|
|
43
|
+
// Called by cron — process all expiring credentials
|
|
44
|
+
static async refreshExpiringCredentials(): Promise<RefreshResult[]>
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Cron Processing Flow
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Cron trigger (every minute)
|
|
52
|
+
→ Query: oauth-refresh-queue WHERE next_refresh_at <= now (limit 100)
|
|
53
|
+
→ For each entry:
|
|
54
|
+
├─ Load credentials from users/{userId}/credentials/{provider}
|
|
55
|
+
├─ Per-provider refresh:
|
|
56
|
+
│ ├─ Google/YouTube: POST oauth2.googleapis.com/token with refresh_token
|
|
57
|
+
│ └─ Instagram: GET graph.instagram.com/refresh_access_token
|
|
58
|
+
├─ Save new credentials (access_token, expires_at)
|
|
59
|
+
├─ Update integration timestamps (last_refreshed_at, next_refresh_at)
|
|
60
|
+
└─ Re-enqueue with new next_refresh_at
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Per-Provider Timing
|
|
64
|
+
|
|
65
|
+
| Provider | Refresh Timing | Token Lifetime |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Google/YouTube | `expiresIn - 600s` (10 min before) | ~1 hour |
|
|
68
|
+
| Instagram | 50 days | 60 days |
|
|
69
|
+
|
|
70
|
+
### Error Handling
|
|
71
|
+
|
|
72
|
+
- Expired/revoked tokens: dequeue + return `{ status: 'failed' }`
|
|
73
|
+
- HTTP errors: log + return `{ status: 'failed', error: httpStatus }`
|
|
74
|
+
- Network errors: log + return `{ status: 'failed' }` (retried next cron cycle)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Checklist
|
|
79
|
+
|
|
80
|
+
- [ ] OAuth callback enqueues refresh with provider-specific timing
|
|
81
|
+
- [ ] Disconnect dequeues the entry
|
|
82
|
+
- [ ] Cron processes max 100 entries per cycle
|
|
83
|
+
- [ ] Failed refreshes are dequeued (user must re-authenticate)
|
|
84
|
+
- [ ] Successful refreshes re-enqueue with next timing
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
**Status**: Stable
|
|
89
|
+
**Last Updated**: 2026-03-14
|
|
90
|
+
**Contributors**: Community
|