diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..cc40198 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,89 @@ +# Configuration for auto-labeling PRs based on changed files + +# Core changes +core: + - changed-files: + - any-glob-to-any-file: + - 'src/core/**/*' + - 'src/interfaces/**/*' + +# Connector changes +connectors: + - changed-files: + - any-glob-to-any-file: + - 'src/connectors/**/*' + - 'src/adapters/**/*' + +# Documentation +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - 'docs/**/*' + - 'examples/**/*' + +# Tests +testing: + - changed-files: + - any-glob-to-any-file: + - '**/__tests__/**/*' + - '**/*.test.ts' + - '**/*.spec.ts' + - 'vitest.config.ts' + +# CI/CD +ci/cd: + - changed-files: + - any-glob-to-any-file: + - '.github/**/*' + - '.gitignore' + - '.npmrc' + +# Dependencies +dependencies: + - changed-files: + - any-glob-to-any-file: + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + +# Platform specific +platform/telegram: + - changed-files: + - any-glob-to-any-file: + - 'src/adapters/telegram/**/*' + - 'src/connectors/messaging/telegram/**/*' + +platform/discord: + - changed-files: + - any-glob-to-any-file: + - 'src/connectors/messaging/discord/**/*' + +platform/cloudflare: + - changed-files: + - any-glob-to-any-file: + - 'wrangler.toml' + - 'src/core/cloud/cloudflare/**/*' + +# Contributions +contribution: + - changed-files: + - any-glob-to-any-file: + - 'contrib/**/*' + - 'src/contrib/**/*' + +# Performance +performance: + - changed-files: + - any-glob-to-any-file: + - 'src/patterns/**/*' + - 'src/lib/cache/**/*' + - '**/performance/**/*' + +# Security +security: + - changed-files: + - any-glob-to-any-file: + - 'src/middleware/auth*.ts' + - 'src/core/security/**/*' + - '**/auth/**/*' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..da9f46e --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,189 @@ +name: PR Validation + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + validate-contribution: + name: Validate Contribution + runs-on: ubuntu-latest + + steps: + - name: Checkout PR + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: TypeScript Check + run: npm run typecheck + + - name: ESLint Check + run: npm run lint + + - name: Run Tests + run: npm test + + - name: Check for Conflicts + run: | + # Check if PR has conflicts with other open PRs + gh pr list --state open --json number,files -q '.[] | select(.number != ${{ github.event.pull_request.number }})' > other_prs.json + + # Get files changed in this PR + gh pr view ${{ github.event.pull_request.number }} --json files -q '.files[].path' > this_pr_files.txt + + # Check for overlapping files + node -e " + const fs = require('fs'); + const otherPRs = JSON.parse(fs.readFileSync('other_prs.json', 'utf8') || '[]'); + const thisPRFiles = fs.readFileSync('this_pr_files.txt', 'utf8').split('\n').filter(Boolean); + + const conflicts = []; + for (const pr of otherPRs) { + const prFiles = (pr.files || []).map(f => f.path); + const overlapping = thisPRFiles.filter(f => prFiles.includes(f)); + if (overlapping.length > 0) { + conflicts.push({ pr: pr.number, files: overlapping }); + } + } + + if (conflicts.length > 0) { + console.log('⚠️ Potential conflicts detected:'); + conflicts.forEach(c => { + console.log(\` PR #\${c.pr}: \${c.files.join(', ')}\`); + }); + process.exit(1); + } + " + env: + GH_TOKEN: ${{ github.token }} + continue-on-error: true + + - name: Check Architecture Compliance + run: | + # Check for platform-specific imports in core modules + echo "Checking for platform-specific imports..." + + # Look for direct platform imports in src/core + if grep -r "from 'grammy'" src/core/ 2>/dev/null || \ + grep -r "from 'discord.js'" src/core/ 2>/dev/null || \ + grep -r "from '@slack/'" src/core/ 2>/dev/null; then + echo "❌ Found platform-specific imports in core modules!" + echo "Please use connector pattern instead." + exit 1 + fi + + echo "✅ No platform-specific imports in core modules" + + - name: Check for Any Types + run: | + # Check for 'any' types in TypeScript files + echo "Checking for 'any' types..." + + # Exclude test files and node_modules + if grep -r ": any" src/ --include="*.ts" --include="*.tsx" \ + --exclude-dir="__tests__" --exclude-dir="node_modules" | \ + grep -v "eslint-disable" | \ + grep -v "@typescript-eslint/no-explicit-any"; then + echo "❌ Found 'any' types without proper justification!" + echo "Please use proper types or add eslint-disable with explanation." + exit 1 + fi + + echo "✅ No unjustified 'any' types found" + + - name: Generate Contribution Report + if: always() + run: | + echo "## 📊 Contribution Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count changes + ADDED=$(git diff --numstat origin/main..HEAD | awk '{sum+=$1} END {print sum}') + DELETED=$(git diff --numstat origin/main..HEAD | awk '{sum+=$2} END {print sum}') + FILES_CHANGED=$(git diff --name-only origin/main..HEAD | wc -l) + + echo "### Changes Summary" >> $GITHUB_STEP_SUMMARY + echo "- Files changed: $FILES_CHANGED" >> $GITHUB_STEP_SUMMARY + echo "- Lines added: $ADDED" >> $GITHUB_STEP_SUMMARY + echo "- Lines deleted: $DELETED" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detect contribution type + if git log --oneline origin/main..HEAD | grep -i "perf:"; then + echo "🚀 **Type**: Performance Optimization" >> $GITHUB_STEP_SUMMARY + elif git log --oneline origin/main..HEAD | grep -i "fix:"; then + echo "🐛 **Type**: Bug Fix" >> $GITHUB_STEP_SUMMARY + elif git log --oneline origin/main..HEAD | grep -i "feat:"; then + echo "✨ **Type**: New Feature" >> $GITHUB_STEP_SUMMARY + else + echo "📝 **Type**: Other" >> $GITHUB_STEP_SUMMARY + fi + + - name: Comment on PR + if: failure() + uses: actions/github-script@v7 + with: + script: | + const message = `## ❌ Validation Failed + + Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + Common issues: + - TypeScript errors or warnings + - ESLint violations + - Failing tests + - Platform-specific imports in core modules + - Unjustified \`any\` types + + Need help? Check our [Contributing Guide](https://github.com/${{ github.repository }}/blob/main/CONTRIBUTING.md).`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); + + label-pr: + name: Auto-label PR + runs-on: ubuntu-latest + if: success() + + steps: + - name: Label based on files + uses: actions/labeler@v5 + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + configuration-path: .github/labeler.yml + + - name: Label based on title + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + const labels = []; + + if (title.includes('perf:')) labels.push('performance'); + if (title.includes('fix:')) labels.push('bug'); + if (title.includes('feat:')) labels.push('enhancement'); + if (title.includes('docs:')) labels.push('documentation'); + if (title.includes('test:')) labels.push('testing'); + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labels + }); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45f908f..bee4563 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -167,10 +167,22 @@ See [Easy Contribute Guide](docs/EASY_CONTRIBUTE.md) for detailed instructions. ## 📚 Resources +### Contribution Guides + +- [Easy Contribute Guide](docs/EASY_CONTRIBUTE.md) - Automated contribution tools +- [Contribution Review Checklist](docs/CONTRIBUTION_REVIEW_CHECKLIST.md) - For maintainers +- [Successful Contributions](docs/SUCCESSFUL_CONTRIBUTIONS.md) - Examples and hall of fame - [Development Workflow](docs/DEVELOPMENT_WORKFLOW.md) - Detailed development guide + +### Technical Documentation + - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) - [grammY Documentation](https://grammy.dev/) - [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/) - [Telegram Bot API](https://core.telegram.org/bots/api) +## 🏆 Recent Successful Contributions + +Check out our [Successful Contributions Gallery](docs/SUCCESSFUL_CONTRIBUTIONS.md) to see real examples of community contributions that made Wireframe better! + Thank you for contributing to make Wireframe the best universal AI assistant platform! diff --git a/README.md b/README.md index ba98546..932f603 100644 --- a/README.md +++ b/README.md @@ -875,6 +875,35 @@ Reusable patterns for common tasks: - **[Command Router](docs/patterns/command-router.js)** - Flexible command routing - **[Access Control](docs/patterns/access-control.js)** - Role-based permissions +## 🎯 Tier-based Optimizations + +Automatic performance optimization based on your Cloudflare plan: + +```typescript +import { createTierOptimizer } from '@/middleware/tier-optimizer'; + +// Auto-detect tier and apply optimizations +app.use( + '*', + createTierOptimizer({ + cacheService: edgeCache, + debug: true, + }), +); + +// Use optimization helpers +const data = await optimizedCache(ctx, 'key', fetchData); +const results = await optimizedBatch(ctx, items, processItems); +``` + +Features: + +- **Automatic Tier Detection**: Identifies Free/Paid/Enterprise plans +- **Resource Tracking**: Monitors CPU, memory, and API usage +- **Dynamic Strategies**: Applies optimizations based on usage +- **Smart Recommendations**: Provides upgrade and optimization tips +- **Graceful Degradation**: Maintains functionality within limits + ## 🚀 Roadmap ### Phase 1: Core Enhancements (Days or Hours) diff --git a/docs/CONTRIBUTION_REVIEW_CHECKLIST.md b/docs/CONTRIBUTION_REVIEW_CHECKLIST.md new file mode 100644 index 0000000..8c505fa --- /dev/null +++ b/docs/CONTRIBUTION_REVIEW_CHECKLIST.md @@ -0,0 +1,184 @@ +# Contribution Review Checklist + +This checklist helps maintainers review contributions from the community consistently and efficiently. + +## 🎯 Core Requirements + +### 1. Code Quality + +- [ ] **TypeScript Strict Mode**: No `any` types, all warnings resolved +- [ ] **ESLint**: Zero errors, minimal warnings with justification +- [ ] **Tests**: New functionality has appropriate test coverage +- [ ] **Documentation**: Changes are documented (code comments, README updates) + +### 2. Architecture Compliance + +- [ ] **Platform Agnostic**: Works across all supported platforms (Telegram, Discord, etc.) +- [ ] **Cloud Independent**: No platform-specific APIs used directly +- [ ] **Connector Pattern**: External services use appropriate connectors +- [ ] **Event-Driven**: Components communicate via EventBus when appropriate + +### 3. Production Readiness + +- [ ] **Error Handling**: Graceful error handling with meaningful messages +- [ ] **Performance**: Optimized for Cloudflare Workers constraints (10ms CPU on free tier) +- [ ] **Type Safety**: Proper type guards for optional values +- [ ] **Backward Compatibility**: No breaking changes without discussion + +## 📋 Review Process + +### Step 1: Initial Check + +```bash +# Check out the PR locally +gh pr checkout + +# Run automated checks +npm run typecheck +npm run lint +npm test +``` + +### Step 2: Code Review + +- [ ] Review changed files for code quality +- [ ] Check for duplicate code or functionality +- [ ] Verify proper error handling +- [ ] Ensure consistent coding style + +### Step 3: Architecture Review + +- [ ] Verify platform independence +- [ ] Check connector pattern usage +- [ ] Review integration points +- [ ] Assess impact on existing features + +### Step 4: Testing + +- [ ] Run existing tests +- [ ] Test new functionality manually +- [ ] Verify edge cases are handled +- [ ] Check performance impact + +## 🚀 Merge Criteria + +### Must Have + +- ✅ All automated checks pass +- ✅ Follows Wireframe architecture patterns +- ✅ Production-tested or thoroughly tested +- ✅ Clear value to the community + +### Nice to Have + +- 📊 Performance benchmarks +- 📝 Migration guide if needed +- 🎯 Example usage +- 🔄 Integration tests + +## 💡 Common Issues to Check + +### 1. Platform Dependencies + +```typescript +// ❌ Bad: Platform-specific +import { TelegramSpecificType } from 'telegram-library'; + +// ✅ Good: Platform-agnostic +import type { MessageContext } from '@/core/interfaces'; +``` + +### 2. Type Safety + +```typescript +// ❌ Bad: Using any +const result = (meta as any).last_row_id; + +// ✅ Good: Proper types +const meta = result.meta as D1RunMeta; +if (!meta.last_row_id) { + throw new Error('No last_row_id returned'); +} +``` + +### 3. Error Handling + +```typescript +// ❌ Bad: Silent failures +try { + await operation(); +} catch { + // Silent fail +} + +// ✅ Good: Proper handling +try { + await operation(); +} catch (error) { + logger.error('Operation failed', { error }); + throw new Error('Meaningful error message'); +} +``` + +## 📝 Response Templates + +### Approved PR + +```markdown +## ✅ Approved! + +Excellent contribution! This PR: + +- Meets all code quality standards +- Follows Wireframe architecture patterns +- Adds valuable functionality +- Is well-tested and documented + +Thank you for contributing to Wireframe! 🚀 +``` + +### Needs Changes + +```markdown +## 📋 Changes Requested + +Thank you for your contribution! Before we can merge, please address: + +1. **[Issue 1]**: [Description and suggested fix] +2. **[Issue 2]**: [Description and suggested fix] + +Feel free to ask questions if anything is unclear! +``` + +### Great But Needs Refactoring + +```markdown +## 🔧 Refactoring Needed + +This is valuable functionality! To align with Wireframe's architecture: + +1. **Make it platform-agnostic**: [Specific suggestions] +2. **Use connector pattern**: [Example structure] +3. **Remove dependencies**: [What to remove/replace] + +Would you like help with the refactoring? +``` + +## 🎉 After Merge + +1. Thank the contributor +2. Update CHANGELOG.md +3. Consider adding to examples +4. Document in release notes +5. Celebrate the contribution! 🎊 + +## 📊 Contribution Quality Metrics + +Track these to improve the contribution process: + +- Time from PR to first review +- Number of review cycles needed +- Common issues found +- Contributor satisfaction + +Remember: Every contribution is valuable, even if it needs refactoring. Be supportive and help contributors succeed! diff --git a/docs/SUCCESSFUL_CONTRIBUTIONS.md b/docs/SUCCESSFUL_CONTRIBUTIONS.md new file mode 100644 index 0000000..9972928 --- /dev/null +++ b/docs/SUCCESSFUL_CONTRIBUTIONS.md @@ -0,0 +1,200 @@ +# Successful Contributions Gallery + +This document showcases successful contributions from the Wireframe community, demonstrating the Bot-Driven Development workflow in action. + +## 🏆 Hall of Fame + +### PR #14: Production Insights from Kogotochki Bot + +**Contributor**: @talkstream +**Date**: July 24, 2025 +**Impact**: 80%+ performance improvement, critical optimizations for free tier + +This contribution brought battle-tested patterns from a production bot with 100+ daily active users: + +#### Contributions: + +1. **CloudPlatform Singleton Pattern** + - Reduced response time from 3-5s to ~500ms + - Critical for Cloudflare Workers free tier (10ms CPU limit) +2. **KV Cache Layer** + - 70% reduction in database queries + - Improved edge performance +3. **Lazy Service Initialization** + - 30% faster cold starts + - 40% less memory usage + +#### Key Takeaway: + +Real production experience revealed performance bottlenecks that weren't apparent during development. The contributor built a bot, hit scaling issues, solved them, and shared the solutions back. + +--- + +### PR #16: D1 Type Safety Interface + +**Contributor**: @talkstream +**Date**: July 25, 2025 +**Impact**: Eliminated all `any` types in database operations + +This contribution solved a critical type safety issue discovered in production: + +#### Problem Solved: + +```typescript +// Before: Unsafe and error-prone +const id = (result.meta as any).last_row_id; + +// After: Type-safe with proper error handling +const meta = result.meta as D1RunMeta; +if (!meta.last_row_id) { + throw new Error('Failed to get last_row_id'); +} +``` + +#### Production Story: + +A silent data loss bug was discovered where `region_id` was undefined after database operations. The root cause was missing type safety for D1 metadata. This pattern prevents such bugs across all Wireframe projects. + +--- + +### PR #17: Universal Notification System (In Progress) + +**Contributor**: @talkstream +**Date**: July 25, 2025 +**Status**: Refactoring for platform independence + +A comprehensive notification system with: + +- Retry logic with exponential backoff +- Batch processing for mass notifications +- User preference management +- Error tracking and monitoring + +#### Lesson Learned: + +Initial implementation was too specific to one bot. Community feedback helped refactor it into a truly universal solution that works across all platforms. + +--- + +## 📊 Contribution Patterns + +### What Makes a Great Contribution? + +1. **Production-Tested** + - Real users exposed edge cases + - Performance issues became apparent at scale + - Solutions are battle-tested + +2. **Universal Application** + - Works across all supported platforms + - Solves common problems every bot faces + - Well-abstracted and reusable + +3. **Clear Documentation** + - Explains the problem clearly + - Shows before/after comparisons + - Includes migration guides + +4. **Measurable Impact** + - Performance metrics (80% faster!) + - Error reduction (0 TypeScript errors) + - User experience improvements + +## 🚀 Success Stories + +### The Kogotochki Journey + +1. **Started**: Building a beauty services marketplace bot +2. **Challenges**: Hit performance walls on free tier +3. **Solutions**: Developed optimization patterns +4. **Contribution**: Shared patterns back to Wireframe +5. **Impact**: All future bots benefit from these optimizations + +### Key Insights: + +- Building real bots reveals real problems +- Production usage drives innovation +- Sharing solutions multiplies impact + +## 💡 Tips for Contributors + +### 1. Start Building + +Don't wait for the "perfect" contribution. Build your bot and contribute as you learn. + +### 2. Document Everything + +- Keep notes on problems you encounter +- Measure performance before/after changes +- Screenshot error messages + +### 3. Think Universal + +Ask yourself: "Would other bots benefit from this?" + +### 4. Share Early + +Even partial solutions can spark discussions and improvements. + +## 🎯 Common Contribution Types + +### Performance Optimizations + +- Caching strategies +- Resource pooling +- Lazy loading +- Connection reuse + +### Type Safety Improvements + +- Interface definitions +- Type guards +- Generic patterns +- Error handling + +### Architecture Patterns + +- Service abstractions +- Connector implementations +- Event handlers +- Middleware + +### Developer Experience + +- CLI tools +- Debugging helpers +- Documentation +- Examples + +## 📈 Impact Metrics + +From our successful contributions: + +- **Response Time**: 3-5s → 500ms (80%+ improvement) +- **Database Queries**: Reduced by 70% +- **Cold Starts**: 30% faster +- **Memory Usage**: 40% reduction +- **Type Errors**: 100% eliminated in affected code + +## 🤝 Join the Community + +Your production experience is valuable! Here's how to contribute: + +1. Build a bot using Wireframe +2. Hit a challenge or limitation +3. Solve it in your bot +4. Run `npm run contribute` +5. Share your solution + +Remember: Every bot you build makes Wireframe better for everyone! + +## 📚 Resources + +- [Contributing Guide](../CONTRIBUTING.md) +- [Easy Contribute Tool](./EASY_CONTRIBUTE.md) +- [Review Checklist](./CONTRIBUTION_REVIEW_CHECKLIST.md) +- [Development Workflow](./DEVELOPMENT_WORKFLOW.md) + +--- + +_Have a success story? Add it here! Your contribution could inspire others._ diff --git a/docs/TIER_OPTIMIZATION.md b/docs/TIER_OPTIMIZATION.md new file mode 100644 index 0000000..3952363 --- /dev/null +++ b/docs/TIER_OPTIMIZATION.md @@ -0,0 +1,479 @@ +# Tier-based Optimization System + +The wireframe platform includes a sophisticated tier-based optimization system that automatically adapts to Cloudflare's different plan limits, ensuring optimal performance regardless of your subscription level. + +## Overview + +Cloudflare Workers have different resource limits based on your plan: + +- **Free Plan**: 10ms CPU, limited subrequests, basic features +- **Paid Plan** ($5/month): 30s CPU, more subrequests, advanced features +- **Enterprise Plan**: Higher limits, premium features + +This optimization system automatically detects your tier and applies appropriate optimizations to maximize performance within your plan's constraints. + +## Features + +- **Automatic Tier Detection**: Detects your Cloudflare plan automatically +- **Resource Tracking**: Monitors CPU, memory, and API usage in real-time +- **Dynamic Optimization**: Applies strategies based on current resource usage +- **Graceful Degradation**: Maintains functionality when approaching limits +- **Smart Recommendations**: Provides actionable insights for optimization + +## Basic Usage + +### Middleware Integration + +```typescript +import { createTierOptimizer } from '@/middleware/tier-optimizer'; +import { EdgeCacheService } from '@/core/services/edge-cache'; + +const app = new Hono(); + +// Initialize with auto-detection +app.use( + '*', + createTierOptimizer({ + cacheService: new EdgeCacheService(), + debug: true, // Enable debug headers + }), +); + +// Or specify tier explicitly +app.use( + '*', + createTierOptimizer({ + tier: 'free', + config: { + aggressive: true, // Enable aggressive optimizations + }, + }), +); +``` + +### Manual Usage + +```typescript +import { TierOptimizationService } from '@/core/services/tier-optimization'; + +const optimizer = new TierOptimizationService('free', { + cache: { + enabled: true, + ttl: 600, // 10 minutes for free tier + }, + batching: { + enabled: true, + size: 5, // Smaller batches for free tier + }, +}); + +// Track usage +optimizer.trackUsage('cpuTime', 5); +optimizer.trackOperation('kv', 'read', 10); + +// Check limits +if (!optimizer.isWithinLimits()) { + console.warn('Approaching resource limits!'); +} + +// Get recommendations +const recommendations = optimizer.getRecommendations(); +``` + +## Optimization Strategies + +### 1. Cache Optimization + +Automatically adjusts cache TTL based on tier: + +```typescript +// Free tier: Aggressive caching +cache.ttl = 600; // 10 minutes +cache.swr = 7200; // 2 hours + +// Paid tier: Balanced caching +cache.ttl = 300; // 5 minutes +cache.swr = 3600; // 1 hour +``` + +### 2. Request Batching + +Batches multiple operations to reduce overhead: + +```typescript +// Automatically enabled when approaching subrequest limits +const results = await optimizedBatch( + ctx, + items, + async (batch) => processItems(batch), + 10, // Default batch size +); +``` + +### 3. Query Simplification + +Reduces query complexity for free tier: + +```typescript +// Free tier: Max complexity 50 +// Paid tier: Max complexity 100 +// Enterprise: No limit +``` + +### 4. Early Termination + +Stops processing when approaching CPU limits: + +```typescript +// Defers non-critical operations +if (cpuUsage > 80%) { + utils.defer(() => backgroundTask()); +} +``` + +### 5. Graceful Degradation + +Reduces functionality to stay within limits: + +```typescript +// Returns simplified responses for free tier +return createTieredResponse(ctx, fullData, { + fullDataTiers: ['paid', 'enterprise'], + summaryFields: ['id', 'name', 'status'], +}); +``` + +## Helper Functions + +### Optimized Cache + +```typescript +import { optimizedCache } from '@/middleware/tier-optimizer'; + +// Automatically adjusts cache times based on tier +const data = await optimizedCache( + ctx, + 'cache-key', + async () => fetchExpensiveData(), + { ttl: 300 }, // Base TTL, adjusted by tier +); +``` + +### Optimized Batch Processing + +```typescript +import { optimizedBatch } from '@/middleware/tier-optimizer'; + +// Automatically adjusts batch size based on tier and resources +const results = await optimizedBatch(ctx, largeDataset, async (batch) => { + // Process batch + return batch.map((item) => transform(item)); +}); +``` + +### Tiered Responses + +```typescript +import { createTieredResponse } from '@/middleware/tier-optimizer'; + +// Returns different data based on tier +app.get('/api/data', async (c) => { + const fullData = await fetchAllData(); + + return createTieredResponse(c, fullData, { + fullDataTiers: ['paid', 'enterprise'], + summaryFields: ['id', 'name', 'created'], + }); +}); +``` + +## Resource Tracking + +### CPU Time + +```typescript +const { result, cpuTime } = await utils.measureCPU(async () => { + return expensiveOperation(); +}); + +console.log(`Operation took ${cpuTime}ms`); +``` + +### KV Operations + +```typescript +// Automatically tracked by middleware +const value = await kv.get('key'); +optimizer.trackOperation('kv', 'read'); + +// Batch KV operations +const values = await Promise.all(keys.map((key) => kv.get(key))); +optimizer.trackOperation('kv', 'read', keys.length); +``` + +### Memory Usage + +```typescript +optimizer.trackUsage('memory', process.memoryUsage().heapUsed / 1024 / 1024); +``` + +## Recommendations System + +The system provides actionable recommendations: + +```typescript +const recommendations = optimizer.getRecommendations(); + +recommendations.forEach((rec) => { + console.log(`[${rec.type}] ${rec.category}: ${rec.message}`); + if (rec.action) { + console.log(` Action: ${rec.action}`); + } +}); +``` + +Example recommendations: + +```json +{ + "type": "critical", + "category": "cpu", + "message": "High CPU usage detected", + "description": "CPU usage is at 85% of the limit", + "impact": 9, + "action": "Consider upgrading to paid plan for 3000x more CPU time", + "metrics": { + "cpuTime": 8.5, + "limit": 10 + } +} +``` + +## Configuration Options + +### Full Configuration + +```typescript +const config: IOptimizationConfig = { + enabled: true, + aggressive: false, // Set true for maximum optimization + + cache: { + enabled: true, + ttl: 300, // Base TTL in seconds + swr: 3600, // Stale-while-revalidate + }, + + batching: { + enabled: true, + size: 10, // Items per batch + timeout: 100, // Batch timeout in ms + }, + + compression: { + enabled: true, + threshold: 1024, // Min size for compression + }, + + queries: { + cache: true, + batch: true, + maxComplexity: 100, + }, +}; +``` + +### Custom Strategies + +```typescript +const customStrategy: IOptimizationStrategy = { + name: 'custom-image-optimization', + description: 'Optimize image processing for tier', + priority: 10, + + shouldApply: (context) => { + return context.tier === 'free' && context.request?.path.includes('/images'); + }, + + apply: (context) => { + // Reduce image quality for free tier + context.config.imageQuality = context.tier === 'free' ? 70 : 90; + }, +}; + +// Register custom strategy +const optimizer = createTierOptimizer({ + strategies: [customStrategy], +}); +``` + +## Best Practices + +### 1. Early Detection + +Detect tier early in the request lifecycle: + +```typescript +app.use( + '*', + createTierOptimizer({ + detectTier: (c) => { + // Custom tier detection logic + if (c.env.PREMIUM_FEATURES) return 'enterprise'; + if (c.env.QUEUES) return 'paid'; + return 'free'; + }, + }), +); +``` + +### 2. Progressive Enhancement + +Build features that work on all tiers: + +```typescript +// Base functionality for all tiers +const baseData = await getEssentialData(); + +// Enhanced data for higher tiers +if (tier !== 'free') { + const extraData = await getEnhancedData(); + return { ...baseData, ...extraData }; +} + +return baseData; +``` + +### 3. Resource Budgeting + +Allocate resources wisely: + +```typescript +const remaining = utils.getRemainingResources(); + +if (remaining.cpuTime > 5) { + // Enough time for complex operation + await complexOperation(); +} else { + // Use cached or simplified result + return cachedResult; +} +``` + +### 4. Monitoring + +Track optimization effectiveness: + +```typescript +app.use( + '*', + createTierOptimizer({ + eventBus, + debug: true, + }), +); + +eventBus.on('optimization:applied', (event) => { + analytics.track('optimization', { + strategy: event.strategy, + tier: event.tier, + }); +}); +``` + +### 5. Testing + +Test with different tier limits: + +```typescript +describe('API endpoints', () => { + it('should work within free tier limits', async () => { + const optimizer = new TierOptimizationService('free'); + + // Test with free tier constraints + const response = await app.request('/api/data'); + + expect(optimizer.isWithinLimits()).toBe(true); + }); +}); +``` + +## Migration Guide + +### From Unoptimized Code + +Before: + +```typescript +// Unoptimized - may exceed free tier limits +app.get('/api/users', async (c) => { + const users = await db.select().from('users').all(); + const enriched = await Promise.all(users.map((user) => enrichUserData(user))); + return c.json(enriched); +}); +``` + +After: + +```typescript +// Optimized for tier limits +app.get('/api/users', async (c) => { + const optimizer = getOptimizationService(c); + + // Use cache for free tier + const users = await optimizedCache(c, 'users-list', async () => { + return db.select().from('users').limit(100).all(); + }); + + // Batch enrichment + const enriched = await optimizedBatch(c, users, async (batch) => { + return Promise.all(batch.map((user) => enrichUserData(user))); + }); + + // Return tiered response + return createTieredResponse(c, enriched, { + summaryFields: ['id', 'name', 'email'], + }); +}); +``` + +## Troubleshooting + +### Common Issues + +1. **"CPU limit exceeded" errors** + - Enable aggressive optimizations + - Reduce batch sizes + - Use more caching + - Consider upgrading tier + +2. **Slow responses on free tier** + - Normal due to CPU limits + - Optimize critical paths + - Pre-compute when possible + +3. **Recommendations not applied** + - Check if optimizations are enabled + - Verify strategy conditions + - Review debug logs + +### Debug Headers + +Enable debug mode to see optimization details: + +``` +X-Tier: free +X-CPU-Usage: 8.5/10ms +X-Memory-Usage: 45.2/128MB +X-Optimization-Count: 3 +``` + +### Monitoring + +Use the event bus for detailed monitoring: + +```typescript +eventBus.on('optimization:*', (event) => { + console.log('Optimization event:', event); +}); +``` diff --git a/examples/tier-optimization-example.ts b/examples/tier-optimization-example.ts new file mode 100644 index 0000000..779012b --- /dev/null +++ b/examples/tier-optimization-example.ts @@ -0,0 +1,497 @@ +/** + * Tier optimization example + */ + +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { + createTierOptimizer, + getOptimizationService, + optimizedCache, + optimizedBatch, + createTieredResponse, +} from '../src/middleware/tier-optimizer'; +import { EdgeCacheService } from '../src/core/services/edge-cache'; +import { EventBus } from '../src/core/events/event-bus'; +import type { CloudflareTier } from '../src/core/interfaces/tier-optimization'; + +// Types +interface User { + id: number; + name: string; + email: string; + profile: { + bio: string; + avatar: string; + social: Record; + }; + stats: { + posts: number; + followers: number; + following: number; + }; +} + +// Create app +const app = new Hono(); + +// Enable CORS +app.use('/*', cors()); + +// Initialize services +const cacheService = new EdgeCacheService({ provider: 'memory' }); +const eventBus = new EventBus(); + +// Add tier optimization middleware +app.use( + '*', + createTierOptimizer({ + cacheService, + eventBus, + debug: true, // Enable debug headers + excludeRoutes: ['/health'], + config: { + cache: { + enabled: true, + ttl: 300, + swr: 3600, + }, + batching: { + enabled: true, + size: 10, + timeout: 100, + }, + }, + }), +); + +// Health check (excluded from optimization) +app.get('/health', (c) => { + return c.json({ status: 'ok' }); +}); + +// Home page with tier info +app.get('/', (c) => { + const optimizer = getOptimizationService(c); + const tier = optimizer?.getCurrentTier() || 'unknown'; + const limits = optimizer?.getTierLimits(); + const usage = optimizer?.getUsage(); + const recommendations = optimizer?.getRecommendations() || []; + + return c.html(` + + + + Tier Optimization Example + + + +

Tier Optimization Example

+ +
+

Current Tier: ${tier.toUpperCase()}

+ +
+
+
CPU Time
+
${usage?.cpuTime || 0}ms / ${limits?.cpuTime}ms
+
+
+
+
+ +
+
Memory
+
${(usage?.memory || 0).toFixed(1)}MB / ${limits?.memory}MB
+
+
+
+
+ +
+
Subrequests
+
${usage?.subrequests || 0} / ${limits?.subrequests}
+
+
+
+
+ +
+
KV Reads
+
${usage?.kvOperations.read || 0} / ${limits?.kvOperations.read}
+
+
+
+
+
+
+ + ${ + recommendations.length > 0 + ? ` +
+

Optimization Recommendations

+ ${recommendations + .map( + (rec) => ` +
+ ${rec.message} + ${rec.description ? `

${rec.description}

` : ''} + ${rec.action ? `

Action: ${rec.action}

` : ''} +
+ `, + ) + .join('')} +
+ ` + : '' + } + +
+

Test Different Operations

+ + + + + + +
+ + + + + + + `); +}); + +// Simple operation +app.get('/api/simple', async (c) => { + const optimizer = getOptimizationService(c); + + // Simulate some work + const start = Date.now(); + const data = Array.from({ length: 10 }, (_, i) => ({ + id: i, + value: Math.random(), + })); + const cpuTime = Date.now() - start; + + optimizer?.trackUsage('cpuTime', cpuTime); + + return c.json({ + operation: 'simple', + tier: optimizer?.getCurrentTier(), + cpuTime, + dataCount: data.length, + data, + }); +}); + +// Complex operation +app.get('/api/complex', async (c) => { + const optimizer = getOptimizationService(c); + + // Simulate complex work + const start = Date.now(); + const users = generateUsers(100); + + // Enrich users (simulated subrequests) + const enriched = await optimizedBatch(c, users, async (batch) => { + optimizer?.trackUsage('subrequests', batch.length); + + // Simulate API calls + return batch.map((user) => ({ + ...user, + enriched: true, + processed: Date.now(), + })); + }); + + const cpuTime = Date.now() - start; + optimizer?.trackUsage('cpuTime', cpuTime); + + // Return tiered response + return createTieredResponse(c, enriched, { + fullDataTiers: ['paid', 'enterprise'], + summaryFields: ['id', 'name', 'email'], + }); +}); + +// Batch operation +app.get('/api/batch', async (c) => { + const optimizer = getOptimizationService(c); + + // Generate items + const items = Array.from({ length: 50 }, (_, i) => ({ id: i, data: `item-${i}` })); + + // Process in batches + const results = await optimizedBatch(c, items, async (batch) => { + // Simulate processing + optimizer?.trackOperation('kv', 'read', batch.length); + + return batch.map((item) => ({ + ...item, + processed: true, + timestamp: Date.now(), + })); + }); + + return c.json({ + operation: 'batch', + tier: optimizer?.getCurrentTier(), + itemCount: items.length, + batchCount: Math.ceil(items.length / 10), + results: results.slice(0, 10), // Return sample + }); +}); + +// Cached operation +app.get('/api/cached', async (c) => { + const optimizer = getOptimizationService(c); + + // Use optimized cache + const data = await optimizedCache( + c, + 'expensive-data', + async () => { + // Simulate expensive operation + optimizer?.trackUsage('cpuTime', 5); + optimizer?.trackOperation('d1', 'read', 10); + + return { + timestamp: Date.now(), + data: generateUsers(20), + expensive: true, + }; + }, + { ttl: 300 }, + ); + + return c.json({ + operation: 'cached', + tier: optimizer?.getCurrentTier(), + cached: data.timestamp !== Date.now(), + data, + }); +}); + +// Simulate different tiers +app.post('/api/simulate-tier/:tier', (c) => { + const tier = c.req.param('tier') as CloudflareTier; + + // In real app, this would be detected automatically + // This is just for demo purposes + c.set('simulatedTier', tier); + + return c.json({ + message: `Simulating ${tier} tier`, + note: 'Refresh page to see changes', + }); +}); + +// Helper functions +function generateUsers(count: number): User[] { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + profile: { + bio: `Bio for user ${i + 1}`, + avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`, + social: { + twitter: `@user${i + 1}`, + github: `user${i + 1}`, + }, + }, + stats: { + posts: Math.floor(Math.random() * 100), + followers: Math.floor(Math.random() * 1000), + following: Math.floor(Math.random() * 500), + }, + })); +} + +// Monitor optimization events +eventBus.on('optimization:applied', (event) => { + console.log('Optimization applied:', event.payload); +}); + +eventBus.on('optimization:error', (event) => { + console.error('Optimization error:', event.payload); +}); + +// Export for Cloudflare Workers +export default app; + +// For local development +if (process.env.NODE_ENV !== 'production') { + const port = 3005; + console.log(`Tier optimization example running at http://localhost:${port}`); + console.log(''); + console.log('This example demonstrates:'); + console.log('- Automatic tier detection and optimization'); + console.log('- Resource usage tracking and limits'); + console.log('- Different optimization strategies'); + console.log('- Tiered responses based on plan'); + console.log(''); + console.log('Try different operations to see how optimizations work!'); +} diff --git a/scripts/contribute.ts b/scripts/contribute.ts index d5cadc4..2a210ca 100644 --- a/scripts/contribute.ts +++ b/scripts/contribute.ts @@ -45,11 +45,52 @@ async function detectWorktree(): Promise { } } +async function checkForExistingPRs(): Promise { + try { + const openPRs = execSync('gh pr list --state open --json files,number,title', { + encoding: 'utf-8', + }); + const prs = JSON.parse(openPRs || '[]'); + + // Get current branch changes + const currentFiles = execSync('git diff --name-only main...HEAD', { + encoding: 'utf-8', + }) + .split('\n') + .filter(Boolean); + + const conflicts: string[] = []; + + for (const pr of prs) { + const prFiles = pr.files || []; + const conflictingFiles = currentFiles.filter((file) => + prFiles.some((prFile: any) => prFile.path === file), + ); + + if (conflictingFiles.length > 0) { + conflicts.push(`PR #${pr.number} "${pr.title}" modifies: ${conflictingFiles.join(', ')}`); + } + } + + return conflicts; + } catch { + return []; + } +} + async function analyzeRecentChanges(): Promise { const spinner = ora('Analyzing recent changes...').start(); const contributions: ContributionType[] = []; try { + // Check for conflicts with existing PRs + const conflicts = await checkForExistingPRs(); + if (conflicts.length > 0) { + spinner.warn('Potential conflicts detected with existing PRs:'); + conflicts.forEach((conflict) => console.log(chalk.yellow(` - ${conflict}`))); + console.log(chalk.blue('\nConsider rebasing after those PRs are merged.\n')); + } + // Get recent changes const diffStat = execSync('git diff --stat HEAD~5..HEAD', { encoding: 'utf-8' }); const recentCommits = execSync('git log --oneline -10', { encoding: 'utf-8' }); @@ -97,6 +138,25 @@ async function analyzeRecentChanges(): Promise { async function createContributionBranch(contribution: ContributionType): Promise { const branchName = `contrib/${contribution.type}-${contribution.title.toLowerCase().replace(/\s+/g, '-')}`; + // Check for conflicts before creating branch + const conflicts = await checkForExistingPRs(); + if (conflicts.length > 0) { + console.log(chalk.yellow('\n⚠️ Warning: Your contribution may conflict with existing PRs')); + const { proceed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'proceed', + message: 'Do you want to continue anyway?', + default: true, + }, + ]); + + if (!proceed) { + console.log(chalk.blue('Consider waiting for existing PRs to be merged first.')); + process.exit(0); + } + } + // Check if we're in a worktree const inWorktree = await detectWorktree(); diff --git a/src/core/interfaces/edge-cache.ts b/src/core/interfaces/edge-cache.ts new file mode 100644 index 0000000..352bf00 --- /dev/null +++ b/src/core/interfaces/edge-cache.ts @@ -0,0 +1,53 @@ +/** + * Edge cache interfaces + */ + +/** + * Cache options + */ +export interface ICacheOptions { + /** + * Time to live in seconds + */ + ttl?: number; + + /** + * Stale while revalidate time in seconds + */ + swr?: number; + + /** + * Cache tags for invalidation + */ + tags?: string[]; +} + +/** + * Edge cache service interface + */ +export interface IEdgeCacheService { + /** + * Get value from cache + */ + get(key: string): Promise; + + /** + * Set value in cache + */ + set(key: string, value: T, options?: ICacheOptions): Promise; + + /** + * Delete value from cache + */ + delete(key: string): Promise; + + /** + * Check if key exists + */ + has(key: string): Promise; + + /** + * Clear all cache or by tags + */ + clear(tags?: string[]): Promise; +} diff --git a/src/core/interfaces/tier-optimization.ts b/src/core/interfaces/tier-optimization.ts new file mode 100644 index 0000000..fd0980c --- /dev/null +++ b/src/core/interfaces/tier-optimization.ts @@ -0,0 +1,392 @@ +/** + * Tier-based optimization interfaces + */ + +/** + * Cloudflare plan tier + */ +export type CloudflareTier = 'free' | 'paid' | 'enterprise'; + +/** + * Resource limits for different tiers + */ +export interface ITierLimits { + /** + * CPU time limit per request (ms) + */ + cpuTime: number; + + /** + * Memory limit (MB) + */ + memory: number; + + /** + * Subrequest limit + */ + subrequests: number; + + /** + * Environment variable size (bytes) + */ + envVarSize: number; + + /** + * Script size (bytes) + */ + scriptSize: number; + + /** + * KV operations per request + */ + kvOperations: { + read: number; + write: number; + delete: number; + list: number; + }; + + /** + * D1 operations per request + */ + d1Operations: { + read: number; + write: number; + }; + + /** + * Queue messages per batch + */ + queueBatchSize: number; + + /** + * Durable Object requests + */ + durableObjectRequests: number; +} + +/** + * Optimization strategy + */ +export interface IOptimizationStrategy { + /** + * Name of the strategy + */ + name: string; + + /** + * Description + */ + description: string; + + /** + * Priority (higher = more important) + */ + priority: number; + + /** + * Check if strategy should be applied + */ + shouldApply: (context: IOptimizationContext) => boolean; + + /** + * Apply the optimization + */ + apply: (context: IOptimizationContext) => void | Promise; +} + +/** + * Optimization context + */ +export interface IOptimizationContext { + /** + * Current tier + */ + tier: CloudflareTier; + + /** + * Resource limits + */ + limits: ITierLimits; + + /** + * Current resource usage + */ + usage: IResourceUsage; + + /** + * Request context + */ + request?: { + method: string; + path: string; + size: number; + }; + + /** + * Configuration options + */ + config: IOptimizationConfig; + + /** + * Helper utilities + */ + utils: IOptimizationUtils; +} + +/** + * Resource usage metrics + */ +export interface IResourceUsage { + /** + * CPU time used (ms) + */ + cpuTime: number; + + /** + * Memory used (MB) + */ + memory: number; + + /** + * Subrequests made + */ + subrequests: number; + + /** + * KV operations + */ + kvOperations: { + read: number; + write: number; + delete: number; + list: number; + }; + + /** + * D1 operations + */ + d1Operations: { + read: number; + write: number; + }; +} + +/** + * Optimization configuration + */ +export interface IOptimizationConfig { + /** + * Enable automatic optimizations + */ + enabled: boolean; + + /** + * Aggressive mode (may affect functionality) + */ + aggressive: boolean; + + /** + * Cache settings + */ + cache: { + /** + * Enable response caching + */ + enabled: boolean; + + /** + * Default TTL (seconds) + */ + ttl: number; + + /** + * Stale-while-revalidate time (seconds) + */ + swr: number; + }; + + /** + * Batch settings + */ + batching: { + /** + * Enable request batching + */ + enabled: boolean; + + /** + * Batch size + */ + size: number; + + /** + * Batch timeout (ms) + */ + timeout: number; + }; + + /** + * Compression settings + */ + compression: { + /** + * Enable response compression + */ + enabled: boolean; + + /** + * Minimum size for compression (bytes) + */ + threshold: number; + }; + + /** + * Query optimization + */ + queries: { + /** + * Enable query result caching + */ + cache: boolean; + + /** + * Enable query batching + */ + batch: boolean; + + /** + * Maximum query complexity + */ + maxComplexity: number; + }; + + /** + * Custom strategies + */ + strategies?: IOptimizationStrategy[]; +} + +/** + * Optimization utilities + */ +export interface IOptimizationUtils { + /** + * Measure CPU time + */ + measureCPU(fn: () => T | Promise): Promise<{ result: T; cpuTime: number }>; + + /** + * Batch operations + */ + batch(items: T[], processor: (batch: T[]) => Promise, batchSize: number): Promise; + + /** + * Cache result + */ + cache(key: string, fn: () => Promise, options?: { ttl?: number; swr?: number }): Promise; + + /** + * Defer operation + */ + defer(fn: () => void | Promise): void; + + /** + * Check remaining resources + */ + getRemainingResources(): { + cpuTime: number; + memory: number; + subrequests: number; + }; +} + +/** + * Tier optimization service + */ +export interface ITierOptimizationService { + /** + * Get current tier + */ + getCurrentTier(): CloudflareTier; + + /** + * Get tier limits + */ + getTierLimits(tier?: CloudflareTier): ITierLimits; + + /** + * Get optimization strategies + */ + getStrategies(): IOptimizationStrategy[]; + + /** + * Apply optimizations + */ + optimize(context: Partial): Promise; + + /** + * Track resource usage + */ + trackUsage(type: keyof IResourceUsage, amount: number): void; + + /** + * Get current usage + */ + getUsage(): IResourceUsage; + + /** + * Reset usage tracking + */ + resetUsage(): void; + + /** + * Check if within limits + */ + isWithinLimits(): boolean; + + /** + * Get recommendations + */ + getRecommendations(): IOptimizationRecommendation[]; +} + +/** + * Optimization recommendation + */ +export interface IOptimizationRecommendation { + /** + * Recommendation type + */ + type: 'warning' | 'suggestion' | 'critical'; + + /** + * Category + */ + category: 'cpu' | 'memory' | 'io' | 'network' | 'cost'; + + /** + * Message + */ + message: string; + + /** + * Detailed description + */ + description?: string; + + /** + * Impact level (1-10) + */ + impact: number; + + /** + * Suggested action + */ + action?: string; + + /** + * Related metrics + */ + metrics?: Record; +} diff --git a/src/core/services/edge-cache/edge-cache-service.ts b/src/core/services/edge-cache/edge-cache-service.ts new file mode 100644 index 0000000..c135371 --- /dev/null +++ b/src/core/services/edge-cache/edge-cache-service.ts @@ -0,0 +1,69 @@ +/** + * Simple edge cache service implementation + */ + +import type { IEdgeCacheService, ICacheOptions } from '../../interfaces/edge-cache'; + +interface CacheEntry { + value: T; + expires: number; + tags?: string[]; +} + +/** + * In-memory edge cache service + */ +export class EdgeCacheService implements IEdgeCacheService { + private cache = new Map>(); + + constructor(_options?: { provider?: 'memory' | 'cloudflare' }) {} + + async get(key: string): Promise { + const entry = this.cache.get(key); + + if (!entry) { + return null; + } + + if (entry.expires < Date.now()) { + this.cache.delete(key); + return null; + } + + return entry.value as T; + } + + async set(key: string, value: T, options?: ICacheOptions): Promise { + const ttl = options?.ttl || 300; // 5 minutes default + const expires = Date.now() + ttl * 1000; + + this.cache.set(key, { + value, + expires, + tags: options?.tags, + }); + } + + async delete(key: string): Promise { + this.cache.delete(key); + } + + async has(key: string): Promise { + const value = await this.get(key); + return value !== null; + } + + async clear(tags?: string[]): Promise { + if (!tags) { + this.cache.clear(); + return; + } + + // Clear by tags + for (const [key, entry] of this.cache) { + if (entry.tags?.some((tag) => tags.includes(tag))) { + this.cache.delete(key); + } + } + } +} diff --git a/src/core/services/edge-cache/index.ts b/src/core/services/edge-cache/index.ts new file mode 100644 index 0000000..b649bec --- /dev/null +++ b/src/core/services/edge-cache/index.ts @@ -0,0 +1,6 @@ +/** + * Edge cache service exports + */ + +export * from './edge-cache-service'; +export type { IEdgeCacheService, ICacheOptions } from '../../interfaces/edge-cache'; diff --git a/src/core/services/tier-optimization/__tests__/tier-optimization-service.test.ts b/src/core/services/tier-optimization/__tests__/tier-optimization-service.test.ts new file mode 100644 index 0000000..e3def94 --- /dev/null +++ b/src/core/services/tier-optimization/__tests__/tier-optimization-service.test.ts @@ -0,0 +1,340 @@ +/** + * Tests for TierOptimizationService + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { TierOptimizationService } from '../tier-optimization-service'; +import type { + IOptimizationStrategy, + IOptimizationContext, + IOptimizationConfig, + IOptimizationUtils, +} from '../../../interfaces/tier-optimization'; + +describe('TierOptimizationService', () => { + let service: TierOptimizationService; + + beforeEach(() => { + service = new TierOptimizationService('free'); + }); + + describe('tier management', () => { + it('should return current tier', () => { + expect(service.getCurrentTier()).toBe('free'); + }); + + it('should return correct limits for free tier', () => { + const limits = service.getTierLimits(); + expect(limits.cpuTime).toBe(10); + expect(limits.memory).toBe(128); + expect(limits.subrequests).toBe(50); + }); + + it('should return correct limits for paid tier', () => { + service = new TierOptimizationService('paid'); + const limits = service.getTierLimits(); + expect(limits.cpuTime).toBe(30000); + expect(limits.memory).toBe(128); + expect(limits.subrequests).toBe(1000); + }); + + it('should return correct limits for enterprise tier', () => { + service = new TierOptimizationService('enterprise'); + const limits = service.getTierLimits(); + expect(limits.cpuTime).toBe(30000); + expect(limits.memory).toBe(512); + expect(limits.subrequests).toBe(5000); + }); + }); + + describe('usage tracking', () => { + it('should track CPU time', () => { + service.trackUsage('cpuTime', 5); + const usage = service.getUsage(); + expect(usage.cpuTime).toBeGreaterThanOrEqual(5); + }); + + it('should track memory usage', () => { + service.trackUsage('memory', 64); + const usage = service.getUsage(); + expect(usage.memory).toBe(64); + }); + + it('should track maximum memory usage', () => { + service.trackUsage('memory', 64); + service.trackUsage('memory', 32); + service.trackUsage('memory', 96); + const usage = service.getUsage(); + expect(usage.memory).toBe(96); + }); + + it('should track operations', () => { + service.trackOperation('kv', 'read', 10); + service.trackOperation('kv', 'write', 5); + service.trackOperation('d1', 'read', 20); + + const usage = service.getUsage(); + expect(usage.kvOperations.read).toBe(10); + expect(usage.kvOperations.write).toBe(5); + expect(usage.d1Operations.read).toBe(20); + }); + + it('should reset usage', () => { + service.trackUsage('cpuTime', 5); + service.trackUsage('memory', 64); + service.trackOperation('kv', 'read', 10); + + service.resetUsage(); + + const usage = service.getUsage(); + expect(usage.memory).toBe(0); + expect(usage.kvOperations.read).toBe(0); + }); + }); + + describe('limit checking', () => { + it('should be within limits initially', () => { + expect(service.isWithinLimits()).toBe(true); + }); + + it('should detect CPU limit exceeded', () => { + service.trackUsage('cpuTime', 15); // Free tier limit is 10ms + expect(service.isWithinLimits()).toBe(false); + }); + + it('should detect memory limit exceeded', () => { + service.trackUsage('memory', 150); // Free tier limit is 128MB + expect(service.isWithinLimits()).toBe(false); + }); + + it('should detect KV operation limit exceeded', () => { + service.trackOperation('kv', 'read', 1500); // Free tier limit is 1000 + expect(service.isWithinLimits()).toBe(false); + }); + }); + + describe('optimization strategies', () => { + it('should apply strategies based on context', async () => { + const mockStrategy: IOptimizationStrategy = { + name: 'test-strategy', + description: 'Test strategy', + priority: 10, + shouldApply: vi.fn().mockReturnValue(true), + apply: vi.fn(), + }; + + service = new TierOptimizationService('free', {}, { strategies: [mockStrategy] }); + + await service.optimize({}); + + expect(mockStrategy.shouldApply).toHaveBeenCalled(); + expect(mockStrategy.apply).toHaveBeenCalled(); + }); + + it('should skip strategies that should not apply', async () => { + const mockStrategy: IOptimizationStrategy = { + name: 'test-strategy', + description: 'Test strategy', + priority: 10, + shouldApply: vi.fn().mockReturnValue(false), + apply: vi.fn(), + }; + + service = new TierOptimizationService('free', {}, { strategies: [mockStrategy] }); + + await service.optimize({}); + + expect(mockStrategy.shouldApply).toHaveBeenCalled(); + expect(mockStrategy.apply).not.toHaveBeenCalled(); + }); + + it('should handle strategy errors gracefully', async () => { + const mockStrategy: IOptimizationStrategy = { + name: 'failing-strategy', + description: 'Failing strategy', + priority: 10, + shouldApply: () => true, + apply: () => { + throw new Error('Strategy failed'); + }, + }; + + service = new TierOptimizationService('free', {}, { strategies: [mockStrategy] }); + + // Should not throw + await expect(service.optimize({})).resolves.not.toThrow(); + }); + }); + + describe('recommendations', () => { + it('should generate CPU usage recommendations', () => { + service.trackUsage('cpuTime', 9); // 90% of free tier limit + + const recommendations = service.getRecommendations(); + const cpuRec = recommendations.find((r) => r.category === 'cpu'); + + expect(cpuRec).toBeDefined(); + expect(cpuRec?.type).toBe('critical'); + expect(cpuRec?.impact).toBe(9); + }); + + it('should generate memory recommendations', () => { + service.trackUsage('memory', 110); // >80% of free tier limit + + const recommendations = service.getRecommendations(); + const memRec = recommendations.find((r) => r.category === 'memory'); + + expect(memRec).toBeDefined(); + expect(memRec?.type).toBe('warning'); + }); + + it('should suggest tier upgrade for free tier', () => { + service.trackUsage('cpuTime', 6); // >50% for free tier + + const recommendations = service.getRecommendations(); + const upgradeRec = recommendations.find((r) => r.category === 'cost'); + + expect(upgradeRec).toBeDefined(); + expect(upgradeRec?.message).toContain('Consider upgrading'); + }); + + it('should suggest batching when subrequests are high', () => { + service = new TierOptimizationService('free', { batching: { enabled: false } }); + service.trackUsage('subrequests', 30); // >50% of free tier limit + + const recommendations = service.getRecommendations(); + const batchRec = recommendations.find((r) => r.category === 'network'); + + expect(batchRec).toBeDefined(); + expect(batchRec?.message).toContain('Enable request batching'); + }); + + it('should sort recommendations by impact', () => { + service.trackUsage('cpuTime', 9); // Critical + service.trackUsage('memory', 110); // Warning + + const recommendations = service.getRecommendations(); + + expect(recommendations[0].impact).toBeGreaterThanOrEqual(recommendations[1].impact); + }); + }); + + describe('optimization utilities', () => { + it('should measure CPU time', async () => { + const context: IOptimizationContext = { + tier: 'free', + limits: service.getTierLimits(), + usage: service.getUsage(), + config: { + enabled: true, + aggressive: false, + cache: { enabled: true, ttl: 300, swr: 3600 }, + batching: { enabled: false, size: 10, timeout: 100 }, + compression: { enabled: false, threshold: 1024 }, + queries: { cache: false, batch: false, maxComplexity: 100 }, + } as IOptimizationConfig, + utils: null as unknown as IOptimizationUtils, + }; + + await service.optimize(context); + + // Utils should be created + expect(context.utils).toBeDefined(); + + const { result, cpuTime } = await context.utils.measureCPU(() => { + return 'test'; + }); + + expect(result).toBe('test'); + expect(cpuTime).toBeGreaterThanOrEqual(0); + }); + + it('should batch operations', async () => { + const context: IOptimizationContext = { + tier: 'free', + limits: service.getTierLimits(), + usage: service.getUsage(), + config: { + enabled: true, + aggressive: false, + cache: { enabled: true, ttl: 300, swr: 3600 }, + batching: { enabled: false, size: 10, timeout: 100 }, + compression: { enabled: false, threshold: 1024 }, + queries: { cache: false, batch: false, maxComplexity: 100 }, + } as IOptimizationConfig, + utils: null as unknown as IOptimizationUtils, + }; + + await service.optimize(context); + + const items = [1, 2, 3, 4, 5]; + const processor = vi + .fn() + .mockImplementation((batch) => Promise.resolve(batch.map((n: number) => n * 2))); + + const results = await context.utils.batch(items, processor, 2); + + expect(results).toEqual([2, 4, 6, 8, 10]); + expect(processor).toHaveBeenCalledTimes(3); // 3 batches: [1,2], [3,4], [5] + }); + + it('should defer tasks', async () => { + const context: IOptimizationContext = { + tier: 'free', + limits: service.getTierLimits(), + usage: service.getUsage(), + config: { + enabled: true, + aggressive: false, + cache: { enabled: true, ttl: 300, swr: 3600 }, + batching: { enabled: false, size: 10, timeout: 100 }, + compression: { enabled: false, threshold: 1024 }, + queries: { cache: false, batch: false, maxComplexity: 100 }, + } as IOptimizationConfig, + utils: null as unknown as IOptimizationUtils, + }; + + await service.optimize(context); + + const deferredFn = vi.fn(); + context.utils.defer(deferredFn); + + // Should not be called immediately + expect(deferredFn).not.toHaveBeenCalled(); + + // Should be called after optimization + await service.optimize(context); + expect(deferredFn).toHaveBeenCalled(); + }); + + it('should calculate remaining resources', async () => { + service.trackUsage('cpuTime', 5); + service.trackUsage('memory', 64); + service.trackUsage('subrequests', 10); + + const context: IOptimizationContext = { + tier: 'free', + limits: service.getTierLimits(), + usage: service.getUsage(), + config: { + enabled: true, + aggressive: false, + cache: { enabled: true, ttl: 300, swr: 3600 }, + batching: { enabled: false, size: 10, timeout: 100 }, + compression: { enabled: false, threshold: 1024 }, + queries: { cache: false, batch: false, maxComplexity: 100 }, + } as IOptimizationConfig, + utils: null as unknown as IOptimizationUtils, + }; + + await service.optimize(context); + + const remaining = context.utils.getRemainingResources(); + + expect(remaining.cpuTime).toBeLessThanOrEqual(5); // ~5ms remaining + expect(remaining.memory).toBe(64); // 64MB remaining + expect(remaining.subrequests).toBe(40); // 40 remaining + }); + }); +}); diff --git a/src/core/services/tier-optimization/index.ts b/src/core/services/tier-optimization/index.ts new file mode 100644 index 0000000..638f9cc --- /dev/null +++ b/src/core/services/tier-optimization/index.ts @@ -0,0 +1,19 @@ +/** + * Tier optimization service exports + */ + +export * from './tier-optimization-service'; +export * from './optimization-strategies'; + +// Re-export interfaces for convenience +export type { + CloudflareTier, + ITierLimits, + IOptimizationStrategy, + IOptimizationContext, + IResourceUsage, + IOptimizationConfig, + IOptimizationUtils, + ITierOptimizationService, + IOptimizationRecommendation, +} from '../../interfaces/tier-optimization'; diff --git a/src/core/services/tier-optimization/optimization-strategies.ts b/src/core/services/tier-optimization/optimization-strategies.ts new file mode 100644 index 0000000..0b3aa4c --- /dev/null +++ b/src/core/services/tier-optimization/optimization-strategies.ts @@ -0,0 +1,278 @@ +/** + * Built-in optimization strategies + */ + +import type { IOptimizationStrategy } from '../../interfaces/tier-optimization'; + +/** + * Cache optimization strategy + */ +export const cacheOptimizationStrategy: IOptimizationStrategy = { + name: 'cache-optimization', + description: 'Aggressive caching for free tier', + priority: 10, + + shouldApply: (context) => { + return context.tier === 'free' && context.config.cache.enabled; + }, + + apply: (context) => { + // For free tier, use more aggressive caching + if (context.tier === 'free') { + context.config.cache.ttl = 600; // 10 minutes + context.config.cache.swr = 7200; // 2 hours + } + }, +}; + +/** + * Request batching strategy + */ +export const batchingStrategy: IOptimizationStrategy = { + name: 'request-batching', + description: 'Batch multiple operations to reduce overhead', + priority: 9, + + shouldApply: (context) => { + const usage = context.usage; + const limits = context.limits; + + // Apply if approaching subrequest limits + return usage.subrequests > limits.subrequests * 0.5; + }, + + apply: (context) => { + context.config.batching.enabled = true; + + // Adjust batch size based on tier + if (context.tier === 'free') { + context.config.batching.size = 5; + context.config.batching.timeout = 50; + } else { + context.config.batching.size = 20; + context.config.batching.timeout = 100; + } + }, +}; + +/** + * Query simplification strategy + */ +export const querySimplificationStrategy: IOptimizationStrategy = { + name: 'query-simplification', + description: 'Simplify complex queries for free tier', + priority: 8, + + shouldApply: (context) => { + return context.tier === 'free' && context.config.queries.maxComplexity > 50; + }, + + apply: (context) => { + if (context.tier === 'free') { + context.config.queries.maxComplexity = 50; + context.config.queries.cache = true; + context.config.queries.batch = true; + } + }, +}; + +/** + * Response compression strategy + */ +export const compressionStrategy: IOptimizationStrategy = { + name: 'response-compression', + description: 'Enable compression to reduce bandwidth', + priority: 7, + + shouldApply: (context) => { + return context.config.compression.enabled && context.request !== undefined; + }, + + apply: (context) => { + // Lower compression threshold for free tier + if (context.tier === 'free') { + context.config.compression.threshold = 512; // 512 bytes + } + }, +}; + +/** + * Early termination strategy + */ +export const earlyTerminationStrategy: IOptimizationStrategy = { + name: 'early-termination', + description: 'Terminate processing early when approaching limits', + priority: 15, + + shouldApply: (context) => { + const cpuUsage = context.usage.cpuTime / context.limits.cpuTime; + return cpuUsage > 0.8; // 80% of CPU limit + }, + + apply: async (context) => { + // Defer non-critical operations + if (context.request?.method === 'GET') { + // For GET requests, return cached data if available + if (context.utils) { + context.utils.defer(() => { + console.info('Deferred background processing due to CPU limits'); + }); + } + } + }, +}; + +/** + * Memory optimization strategy + */ +export const memoryOptimizationStrategy: IOptimizationStrategy = { + name: 'memory-optimization', + description: 'Optimize memory usage', + priority: 8, + + shouldApply: (context) => { + const memoryUsage = context.usage.memory / context.limits.memory; + return memoryUsage > 0.7; // 70% of memory limit + }, + + apply: (context) => { + // Enable aggressive garbage collection hints + if (global.gc) { + global.gc(); + } + + // Reduce batch sizes to lower memory usage + if (context.config.batching.size > 5) { + context.config.batching.size = 5; + } + }, +}; + +/** + * KV operation optimization + */ +export const kvOptimizationStrategy: IOptimizationStrategy = { + name: 'kv-optimization', + description: 'Optimize KV operations', + priority: 9, + + shouldApply: (context) => { + const kvReads = context.usage.kvOperations.read; + const kvLimit = context.limits.kvOperations.read; + return kvReads > kvLimit * 0.6; // 60% of KV read limit + }, + + apply: async (context) => { + // Enable local caching for KV operations + context.config.cache.enabled = true; + + // Batch KV operations when possible + context.config.batching.enabled = true; + + // Warning about high KV usage + console.warn( + `High KV usage: ${context.usage.kvOperations.read}/${context.limits.kvOperations.read} reads`, + ); + }, +}; + +/** + * Adaptive timeout strategy + */ +export const adaptiveTimeoutStrategy: IOptimizationStrategy = { + name: 'adaptive-timeout', + description: 'Adjust timeouts based on remaining CPU time', + priority: 6, + + shouldApply: (context) => { + return context.tier === 'free'; + }, + + apply: (context) => { + if (!context.utils) { + return; + } + + const remaining = context.utils.getRemainingResources(); + + // Set aggressive timeouts for free tier + if (remaining.cpuTime < 3) { + // Less than 3ms remaining + console.warn('Very low CPU time remaining, using minimal timeouts'); + } + }, +}; + +/** + * Graceful degradation strategy + */ +export const gracefulDegradationStrategy: IOptimizationStrategy = { + name: 'graceful-degradation', + description: 'Reduce functionality when approaching limits', + priority: 5, + + shouldApply: (context) => { + const usage = context.usage; + const limits = context.limits; + + // Apply when any resource is above 90% + return ( + usage.cpuTime > limits.cpuTime * 0.9 || + usage.memory > limits.memory * 0.9 || + usage.subrequests > limits.subrequests * 0.9 + ); + }, + + apply: (context) => { + // Disable non-essential features + if (context.config.aggressive) { + console.warn('Entering graceful degradation mode'); + + // Return simplified responses + if (context.utils) { + context.utils.defer(() => { + console.info('Non-essential operations deferred'); + }); + } + } + }, +}; + +/** + * Subrequest pooling strategy + */ +export const subrequestPoolingStrategy: IOptimizationStrategy = { + name: 'subrequest-pooling', + description: 'Pool and reuse subrequest connections', + priority: 7, + + shouldApply: (context) => { + return context.usage.subrequests > 10; + }, + + apply: (context) => { + // Enable connection pooling for subrequests + context.config.batching.enabled = true; + + // Log optimization + if (context.tier === 'free') { + console.info('Subrequest pooling enabled to conserve resources'); + } + }, +}; + +/** + * Default optimization strategies + */ +export const defaultStrategies: IOptimizationStrategy[] = [ + earlyTerminationStrategy, + cacheOptimizationStrategy, + batchingStrategy, + kvOptimizationStrategy, + querySimplificationStrategy, + memoryOptimizationStrategy, + compressionStrategy, + subrequestPoolingStrategy, + adaptiveTimeoutStrategy, + gracefulDegradationStrategy, +]; diff --git a/src/core/services/tier-optimization/tier-optimization-service.ts b/src/core/services/tier-optimization/tier-optimization-service.ts new file mode 100644 index 0000000..5d27fbc --- /dev/null +++ b/src/core/services/tier-optimization/tier-optimization-service.ts @@ -0,0 +1,468 @@ +/** + * Tier-based optimization service implementation + */ + +import type { + CloudflareTier, + ITierLimits, + IOptimizationStrategy, + IOptimizationContext, + IResourceUsage, + IOptimizationConfig, + IOptimizationUtils, + ITierOptimizationService, + IOptimizationRecommendation, +} from '../../interfaces/tier-optimization'; +import type { IEdgeCacheService } from '../../interfaces/edge-cache'; +import type { EventBus } from '../../events/event-bus'; + +import { defaultStrategies } from './optimization-strategies'; + +/** + * Default tier limits based on Cloudflare documentation + */ +const TIER_LIMITS: Record = { + free: { + cpuTime: 10, // 10ms + memory: 128, // 128MB + subrequests: 50, + envVarSize: 64 * 1024, // 64KB + scriptSize: 1024 * 1024, // 1MB + kvOperations: { + read: 1000, + write: 100, + delete: 100, + list: 100, + }, + d1Operations: { + read: 1000, + write: 100, + }, + queueBatchSize: 100, + durableObjectRequests: 50, + }, + paid: { + cpuTime: 30000, // 30 seconds + memory: 128, // 128MB (same as free) + subrequests: 1000, + envVarSize: 128 * 1024, // 128KB + scriptSize: 10 * 1024 * 1024, // 10MB + kvOperations: { + read: 10000, + write: 1000, + delete: 1000, + list: 1000, + }, + d1Operations: { + read: 10000, + write: 1000, + }, + queueBatchSize: 10000, + durableObjectRequests: 1000, + }, + enterprise: { + cpuTime: 30000, // 30 seconds + memory: 512, // 512MB + subrequests: 5000, + envVarSize: 512 * 1024, // 512KB + scriptSize: 25 * 1024 * 1024, // 25MB + kvOperations: { + read: 50000, + write: 5000, + delete: 5000, + list: 5000, + }, + d1Operations: { + read: 50000, + write: 5000, + }, + queueBatchSize: 50000, + durableObjectRequests: 5000, + }, +}; + +/** + * Default optimization configuration + */ +const DEFAULT_CONFIG: IOptimizationConfig = { + enabled: true, + aggressive: false, + cache: { + enabled: true, + ttl: 300, // 5 minutes + swr: 3600, // 1 hour + }, + batching: { + enabled: true, + size: 10, + timeout: 100, + }, + compression: { + enabled: true, + threshold: 1024, // 1KB + }, + queries: { + cache: true, + batch: true, + maxComplexity: 100, + }, +}; + +/** + * Tier optimization service implementation + */ +export class TierOptimizationService implements ITierOptimizationService { + private tier: CloudflareTier; + private config: IOptimizationConfig; + private strategies: IOptimizationStrategy[]; + private usage: IResourceUsage; + private startTime: number; + private cacheService?: IEdgeCacheService; + private eventBus?: EventBus; + private deferredTasks: Array<() => void | Promise> = []; + + constructor( + tier: CloudflareTier = 'free', + config: Partial = {}, + options?: { + cacheService?: IEdgeCacheService; + eventBus?: EventBus; + strategies?: IOptimizationStrategy[]; + }, + ) { + this.tier = tier; + this.config = { ...DEFAULT_CONFIG, ...config }; + this.strategies = [...defaultStrategies, ...(options?.strategies || [])]; + this.cacheService = options?.cacheService; + this.eventBus = options?.eventBus; + this.startTime = Date.now(); + + // Initialize usage tracking + this.usage = { + cpuTime: 0, + memory: 0, + subrequests: 0, + kvOperations: { read: 0, write: 0, delete: 0, list: 0 }, + d1Operations: { read: 0, write: 0 }, + }; + + // Sort strategies by priority + this.strategies.sort((a, b) => b.priority - a.priority); + } + + getCurrentTier(): CloudflareTier { + return this.tier; + } + + getTierLimits(tier?: CloudflareTier): ITierLimits { + return TIER_LIMITS[tier || this.tier]; + } + + getStrategies(): IOptimizationStrategy[] { + return [...this.strategies]; + } + + async optimize(partialContext: Partial): Promise { + if (!this.config.enabled) { + return; + } + + const context: IOptimizationContext = { + tier: this.tier, + limits: this.getTierLimits(), + usage: { ...this.usage }, + config: this.config, + utils: partialContext.utils || this.createUtils(), + ...partialContext, + }; + + // Ensure utils is created + if (!context.utils) { + context.utils = this.createUtils(); + } + + // If utils was not provided, update the partial context with the created utils + if (partialContext.utils === null && 'utils' in partialContext) { + (partialContext as IOptimizationContext).utils = context.utils; + } + + // Apply applicable strategies + for (const strategy of this.strategies) { + try { + if (strategy.shouldApply(context)) { + await strategy.apply(context); + + this.eventBus?.emit( + 'optimization:applied', + { + strategy: strategy.name, + tier: this.tier, + }, + 'tier-optimization', + ); + } + } catch (error) { + console.error(`Failed to apply optimization strategy ${strategy.name}:`, error); + + this.eventBus?.emit( + 'optimization:error', + { + strategy: strategy.name, + error, + }, + 'tier-optimization', + ); + } + } + + // Process deferred tasks if within limits + await this.processDeferredTasks(); + } + + trackUsage(type: keyof IResourceUsage, amount: number): void { + if (type === 'cpuTime') { + this.usage.cpuTime += amount; + } else if (type === 'memory') { + this.usage.memory = Math.max(this.usage.memory, amount); + } else if (type === 'subrequests') { + this.usage.subrequests += amount; + } else if (type === 'kvOperations' || type === 'd1Operations') { + // These are handled separately + console.warn(`Use trackOperation for ${type}`); + } + } + + /** + * Track specific operations + */ + trackOperation( + type: 'kv' | 'd1', + operation: 'read' | 'write' | 'delete' | 'list', + count: number = 1, + ): void { + if (type === 'kv' && operation in this.usage.kvOperations) { + this.usage.kvOperations[operation as keyof typeof this.usage.kvOperations] += count; + } else if (type === 'd1' && operation in this.usage.d1Operations) { + this.usage.d1Operations[operation as keyof typeof this.usage.d1Operations] += count; + } + } + + getUsage(): IResourceUsage { + return { + ...this.usage, + cpuTime: this.usage.cpuTime + (Date.now() - this.startTime), + }; + } + + resetUsage(): void { + this.usage = { + cpuTime: 0, + memory: 0, + subrequests: 0, + kvOperations: { read: 0, write: 0, delete: 0, list: 0 }, + d1Operations: { read: 0, write: 0 }, + }; + this.startTime = Date.now(); + } + + isWithinLimits(): boolean { + const limits = this.getTierLimits(); + const usage = this.getUsage(); + + // Check CPU time + if (usage.cpuTime >= limits.cpuTime) { + return false; + } + + // Check memory + if (usage.memory >= limits.memory) { + return false; + } + + // Check subrequests + if (usage.subrequests >= limits.subrequests) { + return false; + } + + // Check KV operations + for (const [op, count] of Object.entries(usage.kvOperations)) { + if (count >= limits.kvOperations[op as keyof typeof limits.kvOperations]) { + return false; + } + } + + // Check D1 operations + for (const [op, count] of Object.entries(usage.d1Operations)) { + if (count >= limits.d1Operations[op as keyof typeof limits.d1Operations]) { + return false; + } + } + + return true; + } + + getRecommendations(): IOptimizationRecommendation[] { + const recommendations: IOptimizationRecommendation[] = []; + const limits = this.getTierLimits(); + const usage = this.getUsage(); + + // CPU time recommendations + const cpuUsagePercent = (usage.cpuTime / limits.cpuTime) * 100; + if (cpuUsagePercent > 80) { + recommendations.push({ + type: 'critical', + category: 'cpu', + message: 'High CPU usage detected', + description: `CPU usage is at ${cpuUsagePercent.toFixed(1)}% of the limit`, + impact: 9, + action: + this.tier === 'free' + ? 'Consider upgrading to paid plan for 3000x more CPU time' + : 'Optimize heavy computations or use background jobs', + metrics: { cpuTime: usage.cpuTime, limit: limits.cpuTime }, + }); + } + + // Memory recommendations + if (usage.memory > limits.memory * 0.8) { + recommendations.push({ + type: 'warning', + category: 'memory', + message: 'High memory usage', + description: `Memory usage is at ${((usage.memory / limits.memory) * 100).toFixed(1)}%`, + impact: 7, + action: 'Reduce in-memory data or use external storage', + metrics: { memory: usage.memory, limit: limits.memory }, + }); + } + + // KV operation recommendations + const kvReadPercent = (usage.kvOperations.read / limits.kvOperations.read) * 100; + if (kvReadPercent > 70) { + recommendations.push({ + type: 'suggestion', + category: 'io', + message: 'High KV read operations', + description: `KV reads are at ${kvReadPercent.toFixed(1)}% of the limit`, + impact: 5, + action: 'Enable caching to reduce KV reads', + metrics: { kvReads: usage.kvOperations.read, limit: limits.kvOperations.read }, + }); + } + + // Tier upgrade recommendations + if (this.tier === 'free' && cpuUsagePercent > 50) { + recommendations.push({ + type: 'suggestion', + category: 'cost', + message: 'Consider upgrading to paid plan', + description: 'Your usage patterns suggest you would benefit from a paid plan', + impact: 8, + action: 'Upgrade to paid plan for $5/month to get 3000x more CPU time', + }); + } + + // Batching recommendations + if (!this.config.batching.enabled && usage.subrequests > limits.subrequests * 0.5) { + recommendations.push({ + type: 'suggestion', + category: 'network', + message: 'Enable request batching', + description: 'Batching can reduce the number of subrequests', + impact: 6, + action: 'Enable batching in optimization config', + metrics: { subrequests: usage.subrequests, limit: limits.subrequests }, + }); + } + + return recommendations.sort((a, b) => b.impact - a.impact); + } + + /** + * Create optimization utilities + */ + private createUtils(): IOptimizationUtils { + return { + measureCPU: async (fn: () => T | Promise) => { + const start = Date.now(); + const result = await fn(); + const cpuTime = Date.now() - start; + this.trackUsage('cpuTime', cpuTime); + return { result, cpuTime }; + }, + + batch: async ( + items: T[], + processor: (batch: T[]) => Promise, + batchSize: number, + ) => { + const results: R[] = []; + + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + const batchResults = await processor(batch); + results.push(...batchResults); + } + + return results; + }, + + cache: async ( + key: string, + fn: () => Promise, + options?: { ttl?: number; swr?: number }, + ) => { + if (!this.cacheService || !this.config.cache.enabled) { + return fn(); + } + + const cached = await this.cacheService.get(key); + if (cached !== null) { + return cached; + } + + const result = await fn(); + + await this.cacheService.set(key, result, { + ttl: options?.ttl || this.config.cache.ttl, + swr: options?.swr || this.config.cache.swr, + }); + + return result; + }, + + defer: (fn: () => void | Promise) => { + this.deferredTasks.push(fn); + }, + + getRemainingResources: () => { + const limits = this.getTierLimits(); + const usage = this.getUsage(); + + return { + cpuTime: Math.max(0, limits.cpuTime - usage.cpuTime), + memory: Math.max(0, limits.memory - usage.memory), + subrequests: Math.max(0, limits.subrequests - usage.subrequests), + }; + }, + }; + } + + /** + * Process deferred tasks if resources allow + */ + private async processDeferredTasks(): Promise { + const remaining = this.createUtils().getRemainingResources(); + + while (this.deferredTasks.length > 0 && remaining.cpuTime > 1) { + const task = this.deferredTasks.shift(); + if (task) { + try { + await task(); + } catch (error) { + console.error('Failed to process deferred task:', error); + } + } + } + } +} diff --git a/src/middleware/tier-optimizer.ts b/src/middleware/tier-optimizer.ts new file mode 100644 index 0000000..287a075 --- /dev/null +++ b/src/middleware/tier-optimizer.ts @@ -0,0 +1,344 @@ +/** + * Tier optimization middleware + */ + +import type { Context, Next } from 'hono'; + +import type { + CloudflareTier, + ITierOptimizationService, + IOptimizationConfig, + IOptimizationUtils, +} from '../core/interfaces/tier-optimization'; +import type { IEdgeCacheService } from '../core/interfaces/edge-cache'; +import type { EventBus } from '../core/events/event-bus'; +import { TierOptimizationService } from '../core/services/tier-optimization/tier-optimization-service'; + +interface TierOptimizerOptions { + /** + * Cloudflare tier (auto-detected if not specified) + */ + tier?: CloudflareTier; + + /** + * Optimization configuration + */ + config?: Partial; + + /** + * Edge cache service for caching + */ + cacheService?: IEdgeCacheService; + + /** + * Event bus for notifications + */ + eventBus?: EventBus; + + /** + * Custom tier detection function + */ + detectTier?: (c: Context) => CloudflareTier; + + /** + * Enable detailed logging + */ + debug?: boolean; + + /** + * Routes to exclude from optimization + */ + excludeRoutes?: string[] | RegExp | ((path: string) => boolean); + + /** + * Response interceptor + */ + onResponse?: (c: Context, response: Response) => Response | Promise; +} + +/** + * Detect Cloudflare tier from environment + */ +function detectCloudflareTier(c: Context): CloudflareTier { + // Check for enterprise features + if (c.env?.ENTERPRISE_FEATURES || c.env?.CF_ACCOUNT_TYPE === 'enterprise') { + return 'enterprise'; + } + + // Check for paid features + if ( + c.env?.QUEUES || // Queues are paid-only + c.env?.ANALYTICS_ENGINE || // Analytics Engine is paid-only + c.env?.TRACE_WORKER || // Trace Workers are paid-only + c.env?.CF_ACCOUNT_TYPE === 'paid' + ) { + return 'paid'; + } + + // Check CPU limits (this is approximate) + // In production, you might want to measure actual CPU time + const cpuLimit = c.env?.CPU_LIMIT || c.env?.WORKER_CPU_LIMIT; + if (cpuLimit && parseInt(cpuLimit) > 50) { + return 'paid'; + } + + // Default to free tier + return 'free'; +} + +/** + * Create tier optimization middleware + */ +export function createTierOptimizer(options: TierOptimizerOptions = {}) { + let optimizationService: ITierOptimizationService | null = null; + + const shouldOptimize = (path: string): boolean => { + if (!options.excludeRoutes) return true; + + if (Array.isArray(options.excludeRoutes)) { + return !options.excludeRoutes.includes(path); + } + + if (options.excludeRoutes instanceof RegExp) { + return !options.excludeRoutes.test(path); + } + + if (typeof options.excludeRoutes === 'function') { + return !options.excludeRoutes(path); + } + + return true; + }; + + return async function tierOptimizer(c: Context, next: Next) { + const path = c.req.path; + + // Skip if excluded + if (!shouldOptimize(path)) { + return next(); + } + + // Initialize optimization service + if (!optimizationService) { + const tier = options.tier || options.detectTier?.(c) || detectCloudflareTier(c); + + optimizationService = new TierOptimizationService(tier, options.config, { + cacheService: options.cacheService, + eventBus: options.eventBus, + }); + + if (options.debug) { + console.info(`Tier optimization initialized for ${tier} tier`); + } + } + + // Track request start + const startTime = Date.now(); + const startMemory = process.memoryUsage?.()?.heapUsed || 0; + + // Apply pre-request optimizations + await optimizationService.optimize({ + request: { + method: c.req.method, + path: c.req.path, + size: parseInt(c.req.header('content-length') || '0'), + }, + }); + + // Store service in context for use by handlers + c.set('tierOptimization', optimizationService); + + try { + // Execute handler + await next(); + + // Track resource usage + const cpuTime = Date.now() - startTime; + const memory = (process.memoryUsage?.()?.heapUsed || 0) - startMemory; + + optimizationService.trackUsage('cpuTime', cpuTime); + optimizationService.trackUsage('memory', Math.max(0, memory / (1024 * 1024))); // Convert to MB + + // Apply response optimizations + if (options.onResponse) { + const response = await options.onResponse(c, c.res); + if (response !== c.res) { + c.res = response; + } + } + + // Check if within limits + if (!optimizationService.isWithinLimits()) { + console.warn('Resource limits exceeded:', optimizationService.getUsage()); + } + + // Get recommendations + const recommendations = optimizationService.getRecommendations(); + if (recommendations.length > 0 && options.debug) { + console.info('Optimization recommendations:', recommendations); + } + + // Add optimization headers in debug mode + if (options.debug) { + const usage = optimizationService.getUsage(); + const limits = optimizationService.getTierLimits(); + + c.header('X-Tier', optimizationService.getCurrentTier()); + c.header('X-CPU-Usage', `${usage.cpuTime}/${limits.cpuTime}ms`); + c.header('X-Memory-Usage', `${usage.memory.toFixed(1)}/${limits.memory}MB`); + c.header('X-Optimization-Count', recommendations.length.toString()); + } + } catch (error) { + // Track error + optimizationService.trackUsage('cpuTime', Date.now() - startTime); + throw error; + } finally { + // Reset usage for next request + optimizationService.resetUsage(); + } + }; +} + +/** + * Helper to get optimization service from context + */ +export function getOptimizationService(c: Context): ITierOptimizationService | undefined { + return c.get('tierOptimization'); +} + +/** + * Optimization-aware cache wrapper + */ +export function optimizedCache( + c: Context, + _key: string, + fn: () => Promise, + options?: { ttl?: number; swr?: number }, +): Promise { + const service = getOptimizationService(c); + + if (service) { + const context = { + tier: service.getCurrentTier(), + limits: service.getTierLimits(), + usage: service.getUsage(), + config: { cache: { enabled: true, ttl: 300, swr: 3600 } } as IOptimizationConfig, + utils: { + cache: async (_k: string, f: () => Promise, _o?: { ttl?: number; swr?: number }) => { + // Simple in-memory cache for demo + return f(); + }, + } as IOptimizationUtils, + }; + + // Adjust cache times based on tier + if (context.tier === 'free') { + const _adjustedOptions = { + ttl: options?.ttl ? options.ttl * 2 : 600, // Double TTL for free tier + swr: options?.swr ? options.swr * 2 : 7200, + }; + // Options would be used here in real implementation + } + } + + return fn(); +} + +/** + * Optimization-aware batch processor + */ +export async function optimizedBatch( + c: Context, + items: T[], + processor: (batch: T[]) => Promise, + defaultBatchSize: number = 10, +): Promise { + const service = getOptimizationService(c); + + let batchSize = defaultBatchSize; + + if (service) { + const tier = service.getCurrentTier(); + + // Adjust batch size based on tier + if (tier === 'free') { + batchSize = Math.min(5, defaultBatchSize); // Smaller batches for free tier + } else if (tier === 'enterprise') { + batchSize = Math.min(50, defaultBatchSize * 2); // Larger batches for enterprise + } + + // Further adjust based on remaining resources + const usage = service.getUsage(); + const limits = service.getTierLimits(); + + if (usage.cpuTime > limits.cpuTime * 0.7) { + batchSize = Math.max(1, Math.floor(batchSize / 2)); // Halve batch size if CPU constrained + } + } + + const results: R[] = []; + + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + const batchResults = await processor(batch); + results.push(...batchResults); + + // Track subrequest for each batch + service?.trackUsage('subrequests', 1); + } + + return results; +} + +/** + * Create tier-specific response + */ +export function createTieredResponse( + c: Context, + data: unknown, + options?: { + fullDataTiers?: CloudflareTier[]; + summaryFields?: string[]; + }, +): Response { + const service = getOptimizationService(c); + const tier = service?.getCurrentTier() || 'free'; + + const fullDataTiers = options?.fullDataTiers || ['paid', 'enterprise']; + + // Return full data for higher tiers + if (fullDataTiers.includes(tier)) { + return c.json(data); + } + + // Return summary for free tier + if (options?.summaryFields && Array.isArray(data)) { + const summary = data.map((item: Record) => { + const summaryItem: Record = {}; + for (const field of options.summaryFields) { + if (field in item) { + summaryItem[field] = item[field]; + } + } + return summaryItem; + }); + + return c.json({ + data: summary, + _tier: tier, + _notice: 'Upgrade to paid plan for full data access', + }); + } + + // Return limited data + const limitedData = Array.isArray(data) ? data.slice(0, 10) : data; + + return c.json({ + data: limitedData, + _tier: tier, + _notice: + Array.isArray(data) && data.length > 10 + ? `Showing first 10 items. Upgrade to paid plan for full access.` + : undefined, + }); +}