@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
# Usage-Based Billing Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for metered billing with Stripe.
|
|
4
|
-
|
|
5
|
-
## Track Usage
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/usage.ts
|
|
9
|
-
import Stripe from 'stripe'
|
|
10
|
-
import { prisma } from '@/lib/db'
|
|
11
|
-
|
|
12
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
13
|
-
|
|
14
|
-
export async function recordUsage(
|
|
15
|
-
userId: string,
|
|
16
|
-
metric: string,
|
|
17
|
-
quantity: number
|
|
18
|
-
) {
|
|
19
|
-
// Get subscription with usage meter
|
|
20
|
-
const subscription = await prisma.subscription.findUnique({
|
|
21
|
-
where: { userId },
|
|
22
|
-
include: { user: true }
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
if (!subscription?.stripeSubscriptionId) {
|
|
26
|
-
throw new Error('No active subscription')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Find the metered subscription item
|
|
30
|
-
const stripeSubscription = await stripe.subscriptions.retrieve(
|
|
31
|
-
subscription.stripeSubscriptionId
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
const meteredItem = stripeSubscription.items.data.find(
|
|
35
|
-
item => item.price.recurring?.usage_type === 'metered'
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if (!meteredItem) {
|
|
39
|
-
throw new Error('No metered subscription item found')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Record usage
|
|
43
|
-
await stripe.subscriptionItems.createUsageRecord(meteredItem.id, {
|
|
44
|
-
quantity,
|
|
45
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
46
|
-
action: 'increment'
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// Also store locally for quick access
|
|
50
|
-
await prisma.usageRecord.create({
|
|
51
|
-
data: {
|
|
52
|
-
userId,
|
|
53
|
-
metric,
|
|
54
|
-
quantity,
|
|
55
|
-
timestamp: new Date()
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Usage Middleware
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// lib/usage-middleware.ts
|
|
65
|
-
export async function trackApiUsage(userId: string, endpoint: string) {
|
|
66
|
-
const metric = `api_calls`
|
|
67
|
-
|
|
68
|
-
// Batch usage records
|
|
69
|
-
await prisma.usageBuffer.create({
|
|
70
|
-
data: {
|
|
71
|
-
userId,
|
|
72
|
-
metric,
|
|
73
|
-
endpoint,
|
|
74
|
-
timestamp: new Date()
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Background job to flush usage to Stripe
|
|
80
|
-
export async function flushUsageToStripe() {
|
|
81
|
-
const bufferRecords = await prisma.usageBuffer.groupBy({
|
|
82
|
-
by: ['userId', 'metric'],
|
|
83
|
-
_count: { id: true },
|
|
84
|
-
where: { flushedAt: null }
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
for (const record of bufferRecords) {
|
|
88
|
-
try {
|
|
89
|
-
await recordUsage(record.userId, record.metric, record._count.id)
|
|
90
|
-
|
|
91
|
-
await prisma.usageBuffer.updateMany({
|
|
92
|
-
where: {
|
|
93
|
-
userId: record.userId,
|
|
94
|
-
metric: record.metric,
|
|
95
|
-
flushedAt: null
|
|
96
|
-
},
|
|
97
|
-
data: { flushedAt: new Date() }
|
|
98
|
-
})
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error(`Failed to flush usage for ${record.userId}:`, error)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Get Usage Summary
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// lib/usage.ts
|
|
110
|
-
export async function getUsageSummary(userId: string, period?: { start: Date; end: Date }) {
|
|
111
|
-
const startDate = period?.start ?? startOfMonth(new Date())
|
|
112
|
-
const endDate = period?.end ?? new Date()
|
|
113
|
-
|
|
114
|
-
const usage = await prisma.usageRecord.groupBy({
|
|
115
|
-
by: ['metric'],
|
|
116
|
-
where: {
|
|
117
|
-
userId,
|
|
118
|
-
timestamp: {
|
|
119
|
-
gte: startDate,
|
|
120
|
-
lte: endDate
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
_sum: { quantity: true }
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
return usage.reduce((acc, record) => {
|
|
127
|
-
acc[record.metric] = record._sum.quantity ?? 0
|
|
128
|
-
return acc
|
|
129
|
-
}, {} as Record<string, number>)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function getUsageHistory(
|
|
133
|
-
userId: string,
|
|
134
|
-
metric: string,
|
|
135
|
-
days: number = 30
|
|
136
|
-
) {
|
|
137
|
-
const startDate = subDays(new Date(), days)
|
|
138
|
-
|
|
139
|
-
const records = await prisma.usageRecord.findMany({
|
|
140
|
-
where: {
|
|
141
|
-
userId,
|
|
142
|
-
metric,
|
|
143
|
-
timestamp: { gte: startDate }
|
|
144
|
-
},
|
|
145
|
-
orderBy: { timestamp: 'asc' }
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
// Group by day
|
|
149
|
-
const byDay = records.reduce((acc, record) => {
|
|
150
|
-
const day = format(record.timestamp, 'yyyy-MM-dd')
|
|
151
|
-
acc[day] = (acc[day] ?? 0) + record.quantity
|
|
152
|
-
return acc
|
|
153
|
-
}, {} as Record<string, number>)
|
|
154
|
-
|
|
155
|
-
return byDay
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Usage Limits
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// lib/usage-limits.ts
|
|
163
|
-
import { PLANS, PlanId } from './plans'
|
|
164
|
-
|
|
165
|
-
export const USAGE_LIMITS: Record<PlanId, Record<string, number>> = {
|
|
166
|
-
free: {
|
|
167
|
-
api_calls: 1000,
|
|
168
|
-
ai_tokens: 10000,
|
|
169
|
-
storage_mb: 100
|
|
170
|
-
},
|
|
171
|
-
pro: {
|
|
172
|
-
api_calls: 50000,
|
|
173
|
-
ai_tokens: 500000,
|
|
174
|
-
storage_mb: 5000
|
|
175
|
-
},
|
|
176
|
-
enterprise: {
|
|
177
|
-
api_calls: -1, // unlimited
|
|
178
|
-
ai_tokens: -1,
|
|
179
|
-
storage_mb: -1
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function checkUsageLimit(
|
|
184
|
-
userId: string,
|
|
185
|
-
metric: string
|
|
186
|
-
): Promise<{ allowed: boolean; current: number; limit: number }> {
|
|
187
|
-
const planId = await getUserPlan(userId)
|
|
188
|
-
const limit = USAGE_LIMITS[planId][metric]
|
|
189
|
-
|
|
190
|
-
// Unlimited
|
|
191
|
-
if (limit === -1) {
|
|
192
|
-
return { allowed: true, current: 0, limit: -1 }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const usage = await getUsageSummary(userId)
|
|
196
|
-
const current = usage[metric] ?? 0
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
allowed: current < limit,
|
|
200
|
-
current,
|
|
201
|
-
limit
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export async function enforceUsageLimit(userId: string, metric: string) {
|
|
206
|
-
const { allowed, current, limit } = await checkUsageLimit(userId, metric)
|
|
207
|
-
|
|
208
|
-
if (!allowed) {
|
|
209
|
-
throw new UsageLimitExceededError(metric, current, limit)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Usage Dashboard Component
|
|
215
|
-
|
|
216
|
-
```tsx
|
|
217
|
-
// components/usage/UsageDashboard.tsx
|
|
218
|
-
import { getUsageSummary, getUsageHistory } from '@/lib/usage'
|
|
219
|
-
import { USAGE_LIMITS } from '@/lib/usage-limits'
|
|
220
|
-
import { getUserPlan } from '@/lib/billing'
|
|
221
|
-
|
|
222
|
-
interface Props {
|
|
223
|
-
userId: string
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function UsageDashboard({ userId }: Props) {
|
|
227
|
-
const planId = await getUserPlan(userId)
|
|
228
|
-
const usage = await getUsageSummary(userId)
|
|
229
|
-
const limits = USAGE_LIMITS[planId]
|
|
230
|
-
|
|
231
|
-
const metrics = [
|
|
232
|
-
{ key: 'api_calls', label: 'API Calls' },
|
|
233
|
-
{ key: 'ai_tokens', label: 'AI Tokens' },
|
|
234
|
-
{ key: 'storage_mb', label: 'Storage (MB)' }
|
|
235
|
-
]
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
<div className="grid gap-4 md:grid-cols-3">
|
|
239
|
-
{metrics.map(metric => {
|
|
240
|
-
const current = usage[metric.key] ?? 0
|
|
241
|
-
const limit = limits[metric.key]
|
|
242
|
-
const percentage = limit === -1 ? 0 : (current / limit) * 100
|
|
243
|
-
|
|
244
|
-
return (
|
|
245
|
-
<div key={metric.key} className="rounded-lg border p-4">
|
|
246
|
-
<h3 className="text-sm font-medium text-gray-600">{metric.label}</h3>
|
|
247
|
-
<p className="text-2xl font-bold">
|
|
248
|
-
{current.toLocaleString()}
|
|
249
|
-
{limit !== -1 && (
|
|
250
|
-
<span className="text-sm font-normal text-gray-500">
|
|
251
|
-
{' / '}{limit.toLocaleString()}
|
|
252
|
-
</span>
|
|
253
|
-
)}
|
|
254
|
-
</p>
|
|
255
|
-
{limit !== -1 && (
|
|
256
|
-
<div className="mt-2 h-2 rounded-full bg-gray-200">
|
|
257
|
-
<div
|
|
258
|
-
className={`h-2 rounded-full ${
|
|
259
|
-
percentage > 90 ? 'bg-red-500' :
|
|
260
|
-
percentage > 75 ? 'bg-amber-500' : 'bg-green-500'
|
|
261
|
-
}`}
|
|
262
|
-
style={{ width: `${Math.min(100, percentage)}%` }}
|
|
263
|
-
/>
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
266
|
-
</div>
|
|
267
|
-
)
|
|
268
|
-
})}
|
|
269
|
-
</div>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## When to Use
|
|
275
|
-
|
|
276
|
-
- API rate limits
|
|
277
|
-
- AI token usage
|
|
278
|
-
- Storage quotas
|
|
279
|
-
- Pay-as-you-go billing
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
# Caching Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for application caching.
|
|
4
|
-
|
|
5
|
-
## React Cache
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/cache.ts
|
|
9
|
-
import { cache } from 'react'
|
|
10
|
-
import { prisma } from '@/lib/db'
|
|
11
|
-
|
|
12
|
-
// React cache - deduplicates within a single request
|
|
13
|
-
export const getUser = cache(async (userId: string) => {
|
|
14
|
-
return prisma.user.findUnique({ where: { id: userId } })
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
// Multiple calls within same request = 1 database query
|
|
18
|
-
export async function UserProfile({ userId }: { userId: string }) {
|
|
19
|
-
const user = await getUser(userId) // First call - hits DB
|
|
20
|
-
return <Profile user={user} />
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function UserSidebar({ userId }: { userId: string }) {
|
|
24
|
-
const user = await getUser(userId) // Same request - cached
|
|
25
|
-
return <Sidebar user={user} />
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Next.js Data Cache
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
// Fetch with caching
|
|
33
|
-
async function getData() {
|
|
34
|
-
const res = await fetch('https://api.example.com/data', {
|
|
35
|
-
next: {
|
|
36
|
-
revalidate: 3600, // Cache for 1 hour
|
|
37
|
-
tags: ['data'] // For on-demand revalidation
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
return res.json()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Force fresh data
|
|
44
|
-
async function getFreshData() {
|
|
45
|
-
const res = await fetch('https://api.example.com/data', {
|
|
46
|
-
cache: 'no-store'
|
|
47
|
-
})
|
|
48
|
-
return res.json()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// On-demand revalidation
|
|
52
|
-
import { revalidateTag, revalidatePath } from 'next/cache'
|
|
53
|
-
|
|
54
|
-
export async function updateData() {
|
|
55
|
-
await saveToDatabase()
|
|
56
|
-
|
|
57
|
-
// Revalidate by tag
|
|
58
|
-
revalidateTag('data')
|
|
59
|
-
|
|
60
|
-
// Or by path
|
|
61
|
-
revalidatePath('/dashboard')
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## unstable_cache for Database
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// lib/cache.ts
|
|
69
|
-
import { unstable_cache } from 'next/cache'
|
|
70
|
-
import { prisma } from '@/lib/db'
|
|
71
|
-
|
|
72
|
-
export const getCachedPosts = unstable_cache(
|
|
73
|
-
async (userId: string) => {
|
|
74
|
-
return prisma.post.findMany({
|
|
75
|
-
where: { authorId: userId },
|
|
76
|
-
orderBy: { createdAt: 'desc' },
|
|
77
|
-
take: 10
|
|
78
|
-
})
|
|
79
|
-
},
|
|
80
|
-
['user-posts'], // Cache key prefix
|
|
81
|
-
{
|
|
82
|
-
revalidate: 60, // 1 minute
|
|
83
|
-
tags: ['posts'] // For manual revalidation
|
|
84
|
-
}
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
// With dynamic cache key
|
|
88
|
-
export const getCachedPost = unstable_cache(
|
|
89
|
-
async (postId: string) => {
|
|
90
|
-
return prisma.post.findUnique({
|
|
91
|
-
where: { id: postId },
|
|
92
|
-
include: { author: true }
|
|
93
|
-
})
|
|
94
|
-
},
|
|
95
|
-
['post'], // Prefix - actual key will be ['post', postId]
|
|
96
|
-
{ revalidate: 300, tags: ['posts'] }
|
|
97
|
-
)
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Redis Caching
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// lib/redis.ts
|
|
104
|
-
import { Redis } from '@upstash/redis'
|
|
105
|
-
|
|
106
|
-
const redis = new Redis({
|
|
107
|
-
url: process.env.UPSTASH_REDIS_URL!,
|
|
108
|
-
token: process.env.UPSTASH_REDIS_TOKEN!
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
export async function getCached<T>(
|
|
112
|
-
key: string,
|
|
113
|
-
fetcher: () => Promise<T>,
|
|
114
|
-
ttl: number = 3600
|
|
115
|
-
): Promise<T> {
|
|
116
|
-
// Try cache first
|
|
117
|
-
const cached = await redis.get<T>(key)
|
|
118
|
-
|
|
119
|
-
if (cached !== null) {
|
|
120
|
-
return cached
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Fetch fresh data
|
|
124
|
-
const data = await fetcher()
|
|
125
|
-
|
|
126
|
-
// Cache it
|
|
127
|
-
await redis.setex(key, ttl, data)
|
|
128
|
-
|
|
129
|
-
return data
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Usage
|
|
133
|
-
const user = await getCached(
|
|
134
|
-
`user:${userId}`,
|
|
135
|
-
() => prisma.user.findUnique({ where: { id: userId } }),
|
|
136
|
-
300 // 5 minutes
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
// Invalidation
|
|
140
|
-
export async function invalidateCache(pattern: string) {
|
|
141
|
-
const keys = await redis.keys(pattern)
|
|
142
|
-
if (keys.length > 0) {
|
|
143
|
-
await redis.del(...keys)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Stale-While-Revalidate
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
// lib/swr-cache.ts
|
|
152
|
-
import { Redis } from '@upstash/redis'
|
|
153
|
-
|
|
154
|
-
const redis = new Redis({
|
|
155
|
-
url: process.env.UPSTASH_REDIS_URL!,
|
|
156
|
-
token: process.env.UPSTASH_REDIS_TOKEN!
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
interface CacheEntry<T> {
|
|
160
|
-
data: T
|
|
161
|
-
staleAt: number
|
|
162
|
-
expiresAt: number
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function swrCache<T>(
|
|
166
|
-
key: string,
|
|
167
|
-
fetcher: () => Promise<T>,
|
|
168
|
-
{ staleTime = 60, maxAge = 300 }: { staleTime?: number; maxAge?: number } = {}
|
|
169
|
-
): Promise<T> {
|
|
170
|
-
const now = Date.now()
|
|
171
|
-
|
|
172
|
-
const cached = await redis.get<CacheEntry<T>>(key)
|
|
173
|
-
|
|
174
|
-
// Fresh cache hit
|
|
175
|
-
if (cached && now < cached.staleAt) {
|
|
176
|
-
return cached.data
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Stale cache - return stale data but revalidate in background
|
|
180
|
-
if (cached && now < cached.expiresAt) {
|
|
181
|
-
// Revalidate in background
|
|
182
|
-
revalidateInBackground(key, fetcher, staleTime, maxAge)
|
|
183
|
-
return cached.data
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Cache miss - fetch fresh
|
|
187
|
-
return fetchAndCache(key, fetcher, staleTime, maxAge)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function fetchAndCache<T>(
|
|
191
|
-
key: string,
|
|
192
|
-
fetcher: () => Promise<T>,
|
|
193
|
-
staleTime: number,
|
|
194
|
-
maxAge: number
|
|
195
|
-
): Promise<T> {
|
|
196
|
-
const data = await fetcher()
|
|
197
|
-
const now = Date.now()
|
|
198
|
-
|
|
199
|
-
const entry: CacheEntry<T> = {
|
|
200
|
-
data,
|
|
201
|
-
staleAt: now + staleTime * 1000,
|
|
202
|
-
expiresAt: now + maxAge * 1000
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await redis.setex(key, maxAge, entry)
|
|
206
|
-
return data
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function revalidateInBackground<T>(
|
|
210
|
-
key: string,
|
|
211
|
-
fetcher: () => Promise<T>,
|
|
212
|
-
staleTime: number,
|
|
213
|
-
maxAge: number
|
|
214
|
-
) {
|
|
215
|
-
// Don't await - fire and forget
|
|
216
|
-
fetchAndCache(key, fetcher, staleTime, maxAge).catch(console.error)
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Memoization
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// lib/memoize.ts
|
|
224
|
-
|
|
225
|
-
// Simple memoization
|
|
226
|
-
export function memoize<T extends (...args: any[]) => any>(fn: T): T {
|
|
227
|
-
const cache = new Map()
|
|
228
|
-
|
|
229
|
-
return ((...args: Parameters<T>) => {
|
|
230
|
-
const key = JSON.stringify(args)
|
|
231
|
-
|
|
232
|
-
if (cache.has(key)) {
|
|
233
|
-
return cache.get(key)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const result = fn(...args)
|
|
237
|
-
cache.set(key, result)
|
|
238
|
-
return result
|
|
239
|
-
}) as T
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Async memoization with TTL
|
|
243
|
-
export function memoizeAsync<T extends (...args: any[]) => Promise<any>>(
|
|
244
|
-
fn: T,
|
|
245
|
-
ttl: number = 60000
|
|
246
|
-
): T {
|
|
247
|
-
const cache = new Map<string, { value: Awaited<ReturnType<T>>; expires: number }>()
|
|
248
|
-
|
|
249
|
-
return (async (...args: Parameters<T>) => {
|
|
250
|
-
const key = JSON.stringify(args)
|
|
251
|
-
const now = Date.now()
|
|
252
|
-
|
|
253
|
-
const cached = cache.get(key)
|
|
254
|
-
if (cached && cached.expires > now) {
|
|
255
|
-
return cached.value
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const value = await fn(...args)
|
|
259
|
-
cache.set(key, { value, expires: now + ttl })
|
|
260
|
-
return value
|
|
261
|
-
}) as T
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Usage
|
|
265
|
-
const expensiveCalculation = memoize((x: number, y: number) => {
|
|
266
|
-
// Complex computation
|
|
267
|
-
return x * y
|
|
268
|
-
})
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
## When to Use
|
|
272
|
-
|
|
273
|
-
- Database queries
|
|
274
|
-
- API responses
|
|
275
|
-
- Expensive computations
|
|
276
|
-
- Session data
|