codecruise 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Critical Rules - Fastify
|
|
2
|
+
|
|
3
|
+
Must-follow rules. Violations block PR merge.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Error Handling
|
|
8
|
+
|
|
9
|
+
### CRIT-001: Always use typed errors
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// BAD
|
|
13
|
+
throw new Error('User not found');
|
|
14
|
+
|
|
15
|
+
// GOOD
|
|
16
|
+
import { NotFoundError } from '@/lib/errors';
|
|
17
|
+
throw new NotFoundError('User', userId);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### CRIT-002: Never expose internal errors to clients
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// BAD
|
|
24
|
+
reply.status(500).send({ error: err.message, stack: err.stack });
|
|
25
|
+
|
|
26
|
+
// GOOD
|
|
27
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
28
|
+
if (error instanceof AppError) {
|
|
29
|
+
return reply.status(error.statusCode).send({
|
|
30
|
+
error: { code: error.code, message: error.message }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log internal error, send generic message
|
|
35
|
+
request.log.error(error);
|
|
36
|
+
return reply.status(500).send({
|
|
37
|
+
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### CRIT-003: Always handle async errors
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// BAD - unhandled promise rejection
|
|
46
|
+
fastify.get('/users', async (request) => {
|
|
47
|
+
const users = await db.users.findMany(); // Could throw
|
|
48
|
+
return users;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// GOOD - Fastify handles this automatically with async handlers
|
|
52
|
+
// But for callbacks or event handlers:
|
|
53
|
+
worker.on('error', (error) => {
|
|
54
|
+
logger.error('Worker error', { error });
|
|
55
|
+
// Graceful recovery or shutdown
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Validation
|
|
62
|
+
|
|
63
|
+
### CRIT-004: Always validate request input
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// BAD - no validation
|
|
67
|
+
fastify.post('/users', async (request) => {
|
|
68
|
+
const user = await db.users.create(request.body);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// GOOD - schema validation
|
|
72
|
+
fastify.post('/users', {
|
|
73
|
+
schema: {
|
|
74
|
+
body: zodToJsonSchema(createUserSchema),
|
|
75
|
+
}
|
|
76
|
+
}, async (request) => {
|
|
77
|
+
// request.body is validated
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### CRIT-005: Validate environment variables at startup
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// lib/env.ts
|
|
85
|
+
import { z } from 'zod';
|
|
86
|
+
|
|
87
|
+
const envSchema = z.object({
|
|
88
|
+
DATABASE_URL: z.string().url(),
|
|
89
|
+
REDIS_URL: z.string().url(),
|
|
90
|
+
JWT_SECRET: z.string().min(32),
|
|
91
|
+
NODE_ENV: z.enum(['development', 'test', 'production']),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const env = envSchema.parse(process.env);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Security
|
|
100
|
+
|
|
101
|
+
### CRIT-006: Never log sensitive data
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// BAD
|
|
105
|
+
logger.info('Login attempt', { email, password });
|
|
106
|
+
|
|
107
|
+
// GOOD
|
|
108
|
+
logger.info('Login attempt', { email, hasPassword: !!password });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### CRIT-007: Always use parameterized queries
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// BAD - SQL injection
|
|
115
|
+
const users = await db.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;
|
|
116
|
+
|
|
117
|
+
// GOOD - parameterized (Prisma handles this)
|
|
118
|
+
const users = await db.user.findUnique({ where: { id: userId } });
|
|
119
|
+
|
|
120
|
+
// If raw SQL needed:
|
|
121
|
+
const users = await db.$queryRaw(
|
|
122
|
+
Prisma.sql`SELECT * FROM users WHERE id = ${userId}`
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### CRIT-008: Validate file uploads
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Always check:
|
|
130
|
+
// - File size limits
|
|
131
|
+
// - Allowed MIME types
|
|
132
|
+
// - Sanitize filenames
|
|
133
|
+
|
|
134
|
+
fastify.register(multipart, {
|
|
135
|
+
limits: {
|
|
136
|
+
fileSize: 5 * 1024 * 1024, // 5MB
|
|
137
|
+
files: 1,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// In handler
|
|
142
|
+
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
143
|
+
if (!allowedMimes.includes(file.mimetype)) {
|
|
144
|
+
throw new ValidationError('Invalid file type');
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Resource Management
|
|
151
|
+
|
|
152
|
+
### CRIT-009: Always close connections on shutdown
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// index.ts
|
|
156
|
+
const shutdown = async () => {
|
|
157
|
+
await fastify.close();
|
|
158
|
+
await prisma.$disconnect();
|
|
159
|
+
await redis.quit();
|
|
160
|
+
await rabbitConnection.close();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
process.on('SIGTERM', shutdown);
|
|
165
|
+
process.on('SIGINT', shutdown);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### CRIT-010: Set timeouts on external calls
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// BAD - could hang forever
|
|
172
|
+
const response = await fetch(externalApi);
|
|
173
|
+
|
|
174
|
+
// GOOD - with timeout
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(externalApi, { signal: controller.signal });
|
|
180
|
+
} finally {
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Queue Safety
|
|
188
|
+
|
|
189
|
+
### CRIT-011: Always acknowledge/reject messages
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// BAD - message stuck in limbo
|
|
193
|
+
channel.consume(queue, async (msg) => {
|
|
194
|
+
await processMessage(msg);
|
|
195
|
+
// Forgot to ack!
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// GOOD
|
|
199
|
+
channel.consume(queue, async (msg) => {
|
|
200
|
+
if (!msg) return;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await processMessage(msg);
|
|
204
|
+
channel.ack(msg);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Reject with requeue=false to avoid infinite loop
|
|
207
|
+
channel.nack(msg, false, false);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### CRIT-012: Make queue jobs idempotent
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// BAD - could double-charge
|
|
216
|
+
async function processPayment(job) {
|
|
217
|
+
await chargeCard(job.data.amount);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// GOOD - idempotent
|
|
221
|
+
async function processPayment(job) {
|
|
222
|
+
const existing = await db.payment.findUnique({
|
|
223
|
+
where: { idempotencyKey: job.data.idempotencyKey }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (existing) return existing;
|
|
227
|
+
|
|
228
|
+
return await db.payment.create({
|
|
229
|
+
data: { ...job.data, status: 'completed' }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
```
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Queue Rules - BullMQ & RabbitMQ
|
|
2
|
+
|
|
3
|
+
Patterns for job queues and message brokers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## BullMQ Patterns
|
|
8
|
+
|
|
9
|
+
### QUEUE-001: Define typed job data
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// types/jobs.ts
|
|
13
|
+
export interface EmailJob {
|
|
14
|
+
to: string;
|
|
15
|
+
template: 'welcome' | 'reset-password' | 'notification';
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProcessOrderJob {
|
|
20
|
+
orderId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
items: Array<{ productId: string; quantity: number }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Queue with type
|
|
26
|
+
const emailQueue = new Queue<EmailJob>('email', { connection });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### QUEUE-002: Configure appropriate retry strategies
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Transient failures (network, rate limits)
|
|
33
|
+
await queue.add('send-email', data, {
|
|
34
|
+
attempts: 3,
|
|
35
|
+
backoff: {
|
|
36
|
+
type: 'exponential',
|
|
37
|
+
delay: 1000, // 1s, 2s, 4s
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Critical jobs (payments)
|
|
42
|
+
await queue.add('process-payment', data, {
|
|
43
|
+
attempts: 5,
|
|
44
|
+
backoff: {
|
|
45
|
+
type: 'exponential',
|
|
46
|
+
delay: 5000, // 5s, 10s, 20s, 40s, 80s
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Non-critical (analytics)
|
|
51
|
+
await queue.add('track-event', data, {
|
|
52
|
+
attempts: 1, // Fire and forget
|
|
53
|
+
removeOnComplete: true,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### QUEUE-003: Set job timeouts
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const worker = new Worker<EmailJob>('email', processor, {
|
|
61
|
+
connection,
|
|
62
|
+
lockDuration: 30000, // 30s lock
|
|
63
|
+
stalledInterval: 15000, // Check for stalled every 15s
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Or per-job
|
|
67
|
+
await queue.add('long-task', data, {
|
|
68
|
+
timeout: 60000, // 1 minute max
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### QUEUE-004: Handle worker lifecycle
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const worker = new Worker('email', processor, { connection });
|
|
76
|
+
|
|
77
|
+
// Graceful shutdown
|
|
78
|
+
async function shutdown() {
|
|
79
|
+
await worker.close(); // Waits for current job
|
|
80
|
+
await queue.close();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.on('SIGTERM', shutdown);
|
|
85
|
+
process.on('SIGINT', shutdown);
|
|
86
|
+
|
|
87
|
+
// Error handling
|
|
88
|
+
worker.on('failed', (job, err) => {
|
|
89
|
+
logger.error('Job failed', {
|
|
90
|
+
jobId: job?.id,
|
|
91
|
+
name: job?.name,
|
|
92
|
+
error: err.message,
|
|
93
|
+
attempts: job?.attemptsMade,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
worker.on('error', (err) => {
|
|
98
|
+
logger.error('Worker error', { error: err.message });
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### QUEUE-005: Use job progress for long tasks
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const worker = new Worker('video-process', async (job) => {
|
|
106
|
+
const steps = ['download', 'transcode', 'upload', 'cleanup'];
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < steps.length; i++) {
|
|
109
|
+
await performStep(steps[i], job.data);
|
|
110
|
+
await job.updateProgress((i + 1) / steps.length * 100);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Monitor progress
|
|
115
|
+
const job = await queue.add('process', { videoId: '123' });
|
|
116
|
+
job.on('progress', (progress) => {
|
|
117
|
+
console.log(`Progress: ${progress}%`);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## RabbitMQ Patterns
|
|
124
|
+
|
|
125
|
+
### QUEUE-006: Use durable queues and persistent messages
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Durable queue survives broker restart
|
|
129
|
+
await channel.assertQueue('orders', { durable: true });
|
|
130
|
+
|
|
131
|
+
// Persistent message survives broker restart
|
|
132
|
+
channel.sendToQueue('orders', Buffer.from(JSON.stringify(order)), {
|
|
133
|
+
persistent: true,
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### QUEUE-007: Implement dead letter queues
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Main queue with DLX
|
|
141
|
+
await channel.assertQueue('orders', {
|
|
142
|
+
durable: true,
|
|
143
|
+
arguments: {
|
|
144
|
+
'x-dead-letter-exchange': 'orders.dlx',
|
|
145
|
+
'x-dead-letter-routing-key': 'orders.failed',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Dead letter queue
|
|
150
|
+
await channel.assertExchange('orders.dlx', 'direct', { durable: true });
|
|
151
|
+
await channel.assertQueue('orders.failed', { durable: true });
|
|
152
|
+
await channel.bindQueue('orders.failed', 'orders.dlx', 'orders.failed');
|
|
153
|
+
|
|
154
|
+
// Consumer - failed messages go to DLQ after nack
|
|
155
|
+
channel.consume('orders', async (msg) => {
|
|
156
|
+
try {
|
|
157
|
+
await processOrder(JSON.parse(msg.content.toString()));
|
|
158
|
+
channel.ack(msg);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// nack with requeue=false sends to DLQ
|
|
161
|
+
channel.nack(msg, false, false);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### QUEUE-008: Use prefetch for fair dispatch
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Don't send more than 10 messages to this worker at once
|
|
170
|
+
channel.prefetch(10);
|
|
171
|
+
|
|
172
|
+
// For CPU-intensive tasks, use lower prefetch
|
|
173
|
+
channel.prefetch(1);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### QUEUE-009: Use topic exchanges for flexible routing
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Publisher
|
|
180
|
+
await channel.assertExchange('events', 'topic', { durable: true });
|
|
181
|
+
|
|
182
|
+
channel.publish('events', 'order.created', Buffer.from(JSON.stringify(order)));
|
|
183
|
+
channel.publish('events', 'order.shipped', Buffer.from(JSON.stringify(order)));
|
|
184
|
+
channel.publish('events', 'user.registered', Buffer.from(JSON.stringify(user)));
|
|
185
|
+
|
|
186
|
+
// Consumer - subscribe to patterns
|
|
187
|
+
await channel.assertQueue('order-notifications');
|
|
188
|
+
await channel.bindQueue('order-notifications', 'events', 'order.*');
|
|
189
|
+
|
|
190
|
+
await channel.assertQueue('all-events');
|
|
191
|
+
await channel.bindQueue('all-events', 'events', '#'); // All messages
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### QUEUE-010: Implement request-reply pattern
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Requester
|
|
198
|
+
const replyQueue = await channel.assertQueue('', { exclusive: true });
|
|
199
|
+
const correlationId = uuid();
|
|
200
|
+
|
|
201
|
+
const response = new Promise((resolve) => {
|
|
202
|
+
channel.consume(replyQueue.queue, (msg) => {
|
|
203
|
+
if (msg.properties.correlationId === correlationId) {
|
|
204
|
+
resolve(JSON.parse(msg.content.toString()));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
channel.sendToQueue('rpc-queue', Buffer.from(JSON.stringify(request)), {
|
|
210
|
+
correlationId,
|
|
211
|
+
replyTo: replyQueue.queue,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await response;
|
|
215
|
+
|
|
216
|
+
// Responder
|
|
217
|
+
channel.consume('rpc-queue', async (msg) => {
|
|
218
|
+
const request = JSON.parse(msg.content.toString());
|
|
219
|
+
const response = await processRequest(request);
|
|
220
|
+
|
|
221
|
+
channel.sendToQueue(
|
|
222
|
+
msg.properties.replyTo,
|
|
223
|
+
Buffer.from(JSON.stringify(response)),
|
|
224
|
+
{ correlationId: msg.properties.correlationId }
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
channel.ack(msg);
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Shared Patterns
|
|
234
|
+
|
|
235
|
+
### QUEUE-011: Log job lifecycle
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// BullMQ
|
|
239
|
+
worker.on('active', (job) => {
|
|
240
|
+
logger.info('Job started', { jobId: job.id, name: job.name });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
worker.on('completed', (job, result) => {
|
|
244
|
+
logger.info('Job completed', {
|
|
245
|
+
jobId: job.id,
|
|
246
|
+
duration: Date.now() - job.processedOn!,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// RabbitMQ - wrap consumer
|
|
251
|
+
function createConsumer(handler) {
|
|
252
|
+
return async (msg) => {
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
logger.info('Message received', { queue, deliveryTag: msg.fields.deliveryTag });
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await handler(msg);
|
|
258
|
+
logger.info('Message processed', { duration: Date.now() - start });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.error('Message failed', { error: error.message });
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### QUEUE-012: Use unique job IDs for deduplication
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// BullMQ - same jobId = same job (deduped)
|
|
271
|
+
await queue.add('process', data, {
|
|
272
|
+
jobId: `order-${orderId}`, // Won't duplicate if already exists
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// RabbitMQ - use message ID
|
|
276
|
+
channel.sendToQueue('orders', buffer, {
|
|
277
|
+
messageId: `order-${orderId}-${Date.now()}`,
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### QUEUE-013: Monitor queue health
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// BullMQ
|
|
285
|
+
async function getQueueHealth(queue: Queue) {
|
|
286
|
+
const [waiting, active, completed, failed] = await Promise.all([
|
|
287
|
+
queue.getWaitingCount(),
|
|
288
|
+
queue.getActiveCount(),
|
|
289
|
+
queue.getCompletedCount(),
|
|
290
|
+
queue.getFailedCount(),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
return { waiting, active, completed, failed };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Expose via health endpoint
|
|
297
|
+
fastify.get('/health/queues', async () => {
|
|
298
|
+
return {
|
|
299
|
+
email: await getQueueHealth(emailQueue),
|
|
300
|
+
orders: await getQueueHealth(orderQueue),
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
```
|