@jgamaraalv/ts-dev-kit 1.0.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/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/agents/accessibility-pro.md +139 -0
- package/agents/api-builder.md +110 -0
- package/agents/code-reviewer.md +190 -0
- package/agents/database-expert.md +138 -0
- package/agents/debugger.md +241 -0
- package/agents/docker-expert.md +51 -0
- package/agents/multi-agent-coordinator.md +378 -0
- package/agents/nextjs-expert.md +136 -0
- package/agents/performance-engineer.md +138 -0
- package/agents/playwright-expert.md +126 -0
- package/agents/react-specialist.md +97 -0
- package/agents/security-scanner.md +105 -0
- package/agents/test-generator.md +221 -0
- package/agents/typescript-pro.md +253 -0
- package/agents/ux-optimizer.md +93 -0
- package/docs/rules/orchestration.md.template +126 -0
- package/package.json +28 -0
- package/skills/bullmq/SKILL.md +225 -0
- package/skills/bullmq/references/flows-and-schedulers.md +186 -0
- package/skills/bullmq/references/job-types-and-options.md +163 -0
- package/skills/bullmq/references/patterns.md +273 -0
- package/skills/bullmq/references/production.md +308 -0
- package/skills/composition-patterns/SKILL.md +58 -0
- package/skills/composition-patterns/references/architecture-avoid-boolean-props.md +87 -0
- package/skills/composition-patterns/references/architecture-compound-components.md +107 -0
- package/skills/composition-patterns/references/patterns-children-over-render-props.md +77 -0
- package/skills/composition-patterns/references/patterns-explicit-variants.md +87 -0
- package/skills/composition-patterns/references/react19-no-forwardref.md +37 -0
- package/skills/composition-patterns/references/state-context-interface.md +194 -0
- package/skills/composition-patterns/references/state-decouple-implementation.md +96 -0
- package/skills/composition-patterns/references/state-lift-state.md +126 -0
- package/skills/conventional-commits/SKILL.md +148 -0
- package/skills/docker/SKILL.md +55 -0
- package/skills/docker/references/compose-configs.md +95 -0
- package/skills/docker/references/monorepo-dockerfile.md +111 -0
- package/skills/drizzle-pg/SKILL.md +202 -0
- package/skills/drizzle-pg/references/advanced.md +299 -0
- package/skills/drizzle-pg/references/migrations.md +214 -0
- package/skills/drizzle-pg/references/queries.md +321 -0
- package/skills/drizzle-pg/references/relations.md +272 -0
- package/skills/drizzle-pg/references/schema-pg.md +256 -0
- package/skills/drizzle-pg/references/sql-operator.md +215 -0
- package/skills/fastify-best-practices/SKILL.md +143 -0
- package/skills/fastify-best-practices/references/hooks-and-lifecycle.md +122 -0
- package/skills/fastify-best-practices/references/plugins-and-encapsulation.md +137 -0
- package/skills/fastify-best-practices/references/request-reply-errors.md +189 -0
- package/skills/fastify-best-practices/references/routes-and-handlers.md +134 -0
- package/skills/fastify-best-practices/references/server-and-options.md +127 -0
- package/skills/fastify-best-practices/references/typescript-and-logging.md +223 -0
- package/skills/fastify-best-practices/references/validation-and-serialization.md +190 -0
- package/skills/ioredis/SKILL.md +51 -0
- package/skills/ioredis/references/advanced-patterns.md +312 -0
- package/skills/ioredis/references/cluster-sentinel.md +280 -0
- package/skills/ioredis/references/connection-options.md +187 -0
- package/skills/ioredis/references/core-api.md +179 -0
- package/skills/nextjs-best-practices/SKILL.md +194 -0
- package/skills/nextjs-best-practices/references/async-patterns.md +84 -0
- package/skills/nextjs-best-practices/references/bundling.md +192 -0
- package/skills/nextjs-best-practices/references/data-patterns.md +310 -0
- package/skills/nextjs-best-practices/references/debug-tricks.md +127 -0
- package/skills/nextjs-best-practices/references/directives.md +74 -0
- package/skills/nextjs-best-practices/references/error-handling.md +237 -0
- package/skills/nextjs-best-practices/references/file-conventions.md +152 -0
- package/skills/nextjs-best-practices/references/font.md +175 -0
- package/skills/nextjs-best-practices/references/functions.md +116 -0
- package/skills/nextjs-best-practices/references/hydration-error.md +86 -0
- package/skills/nextjs-best-practices/references/image.md +184 -0
- package/skills/nextjs-best-practices/references/metadata.md +305 -0
- package/skills/nextjs-best-practices/references/parallel-routes.md +299 -0
- package/skills/nextjs-best-practices/references/route-handlers.md +154 -0
- package/skills/nextjs-best-practices/references/rsc-boundaries.md +168 -0
- package/skills/nextjs-best-practices/references/runtime-selection.md +40 -0
- package/skills/nextjs-best-practices/references/scripts.md +148 -0
- package/skills/nextjs-best-practices/references/self-hosting.md +210 -0
- package/skills/nextjs-best-practices/references/suspense-boundaries.md +67 -0
- package/skills/owasp-security-review/SKILL.md +98 -0
- package/skills/owasp-security-review/references/a01-broken-access-control.md +78 -0
- package/skills/owasp-security-review/references/a02-security-misconfiguration.md +81 -0
- package/skills/owasp-security-review/references/a03-supply-chain-failures.md +65 -0
- package/skills/owasp-security-review/references/a04-cryptographic-failures.md +82 -0
- package/skills/owasp-security-review/references/a05-injection.md +106 -0
- package/skills/owasp-security-review/references/a06-insecure-design.md +76 -0
- package/skills/owasp-security-review/references/a07-authentication-failures.md +83 -0
- package/skills/owasp-security-review/references/a08-integrity-failures.md +72 -0
- package/skills/owasp-security-review/references/a09-logging-alerting-failures.md +76 -0
- package/skills/owasp-security-review/references/a10-exceptional-conditions.md +131 -0
- package/skills/postgresql/SKILL.md +50 -0
- package/skills/postgresql/references/ddl-schema.md +300 -0
- package/skills/postgresql/references/indexes.md +257 -0
- package/skills/postgresql/references/jsonb.md +261 -0
- package/skills/postgresql/references/performance.md +291 -0
- package/skills/postgresql/references/psql-cli.md +153 -0
- package/skills/postgresql/references/queries.md +287 -0
- package/skills/postgresql/references/transactions.md +280 -0
- package/skills/react-best-practices/SKILL.md +110 -0
- package/skills/react-best-practices/references/advanced-patterns.md +91 -0
- package/skills/react-best-practices/references/async-patterns.md +233 -0
- package/skills/react-best-practices/references/bundle-optimization.md +201 -0
- package/skills/react-best-practices/references/client-patterns.md +178 -0
- package/skills/react-best-practices/references/js-performance.md +210 -0
- package/skills/react-best-practices/references/rendering-performance.md +209 -0
- package/skills/react-best-practices/references/rerender-optimization.md +316 -0
- package/skills/react-best-practices/references/server-performance.md +274 -0
- package/skills/service-worker/SKILL.md +195 -0
- package/skills/service-worker/references/api-reference.md +114 -0
- package/skills/service-worker/references/caching-strategies.md +202 -0
- package/skills/service-worker/references/push-and-sync.md +261 -0
- package/skills/typescript-conventions/SKILL.md +51 -0
- package/skills/ui-ux-guidelines/SKILL.md +105 -0
- package/skills/ui-ux-guidelines/references/accessibility-and-interaction.md +74 -0
- package/skills/ui-ux-guidelines/references/forms-content-checklist.md +126 -0
- package/skills/ui-ux-guidelines/references/layout-typography-animation.md +95 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Process Step Jobs](#process-step-jobs)
|
|
6
|
+
- [Idempotent Jobs](#idempotent-jobs)
|
|
7
|
+
- [Throttle Jobs](#throttle-jobs)
|
|
8
|
+
- [Manual Rate Limiting](#manual-rate-limiting)
|
|
9
|
+
- [Static Rate Limiting](#static-rate-limiting)
|
|
10
|
+
- [Global Concurrency](#global-concurrency)
|
|
11
|
+
- [Failing Fast When Redis Is Down](#failing-fast-when-redis-is-down)
|
|
12
|
+
- [Redis Cluster](#redis-cluster)
|
|
13
|
+
|
|
14
|
+
## Process Step Jobs
|
|
15
|
+
|
|
16
|
+
Break processor logic into resumable steps. Save step state in `job.data` so retries resume from the correct step.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Worker } from "bullmq";
|
|
20
|
+
|
|
21
|
+
enum Step {
|
|
22
|
+
Initial,
|
|
23
|
+
Second,
|
|
24
|
+
Finish,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const worker = new Worker(
|
|
28
|
+
"queueName",
|
|
29
|
+
async (job) => {
|
|
30
|
+
let step = job.data.step;
|
|
31
|
+
while (step !== Step.Finish) {
|
|
32
|
+
switch (step) {
|
|
33
|
+
case Step.Initial: {
|
|
34
|
+
await doInitialStepStuff();
|
|
35
|
+
await job.updateData({ step: Step.Second });
|
|
36
|
+
step = Step.Second;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case Step.Second: {
|
|
40
|
+
await doSecondStepStuff();
|
|
41
|
+
await job.updateData({ step: Step.Finish });
|
|
42
|
+
step = Step.Finish;
|
|
43
|
+
return Step.Finish;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
throw new Error("invalid step");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{ connection },
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Delaying Mid-Process
|
|
55
|
+
|
|
56
|
+
Use `moveToDelayed` + `DelayedError` to pause a job and resume later:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { DelayedError, Worker } from "bullmq";
|
|
60
|
+
|
|
61
|
+
const worker = new Worker(
|
|
62
|
+
"queueName",
|
|
63
|
+
async (job, token) => {
|
|
64
|
+
let step = job.data.step;
|
|
65
|
+
switch (step) {
|
|
66
|
+
case Step.Initial: {
|
|
67
|
+
await doStuff();
|
|
68
|
+
await job.moveToDelayed(Date.now() + 5000, token);
|
|
69
|
+
await job.updateData({ step: Step.Second });
|
|
70
|
+
throw new DelayedError(); // signals worker to release the job
|
|
71
|
+
}
|
|
72
|
+
case Step.Second: {
|
|
73
|
+
// resumes here after 5s delay
|
|
74
|
+
return doMoreStuff();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{ connection },
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Waiting for Dynamic Children
|
|
83
|
+
|
|
84
|
+
Add children at runtime, then wait for them with `moveToWaitingChildren`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { WaitingChildrenError, Worker } from "bullmq";
|
|
88
|
+
|
|
89
|
+
const worker = new Worker(
|
|
90
|
+
"parentQueue",
|
|
91
|
+
async (job, token) => {
|
|
92
|
+
let step = job.data.step;
|
|
93
|
+
switch (step) {
|
|
94
|
+
case Step.Initial: {
|
|
95
|
+
// Add child dynamically
|
|
96
|
+
await childQueue.add(
|
|
97
|
+
"child-1",
|
|
98
|
+
{ foo: "bar" },
|
|
99
|
+
{
|
|
100
|
+
parent: { id: job.id!, queue: job.queueQualifiedName },
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
await job.updateData({ step: Step.WaitForChildren });
|
|
104
|
+
step = Step.WaitForChildren;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case Step.WaitForChildren: {
|
|
108
|
+
const shouldWait = await job.moveToWaitingChildren(token!);
|
|
109
|
+
if (shouldWait) {
|
|
110
|
+
throw new WaitingChildrenError();
|
|
111
|
+
}
|
|
112
|
+
// All children done — continue
|
|
113
|
+
return "done";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{ connection },
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Chaining Flows at Runtime
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { FlowProducer, WaitingChildrenError, Worker } from "bullmq";
|
|
125
|
+
|
|
126
|
+
const flow = new FlowProducer({ connection });
|
|
127
|
+
|
|
128
|
+
const worker = new Worker(
|
|
129
|
+
"parentQueue",
|
|
130
|
+
async (job, token) => {
|
|
131
|
+
// Add a full flow as children
|
|
132
|
+
await flow.add({
|
|
133
|
+
name: "child-job",
|
|
134
|
+
queueName: "childQueue",
|
|
135
|
+
data: {},
|
|
136
|
+
children: [
|
|
137
|
+
{ name: "gc1", data: {}, queueName: "grandchildQueue" },
|
|
138
|
+
{ name: "gc2", data: {}, queueName: "grandchildQueue" },
|
|
139
|
+
],
|
|
140
|
+
opts: {
|
|
141
|
+
parent: { id: job.id!, queue: job.queueQualifiedName },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const shouldWait = await job.moveToWaitingChildren(token!);
|
|
146
|
+
if (shouldWait) throw new WaitingChildrenError();
|
|
147
|
+
return "all children done";
|
|
148
|
+
},
|
|
149
|
+
{ connection },
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Note:** `DelayedError`, `WaitingChildrenError`, and `RateLimitError` do NOT increment `attemptsMade`. Use `maxStartedAttempts` to limit how many times a job can start processing.
|
|
154
|
+
|
|
155
|
+
## Idempotent Jobs
|
|
156
|
+
|
|
157
|
+
Use `jobId` to prevent duplicate processing:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await queue.add(
|
|
161
|
+
"process-order",
|
|
162
|
+
{ orderId: 123 },
|
|
163
|
+
{
|
|
164
|
+
jobId: "order-123",
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
// Second add with same jobId is silently ignored
|
|
168
|
+
await queue.add(
|
|
169
|
+
"process-order",
|
|
170
|
+
{ orderId: 123 },
|
|
171
|
+
{
|
|
172
|
+
jobId: "order-123",
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Throttle Jobs
|
|
178
|
+
|
|
179
|
+
Use deduplication throttle mode to limit how often a job type runs:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
await queue.add("sync", data, {
|
|
183
|
+
deduplication: { id: "sync-user-42", ttl: 60000 },
|
|
184
|
+
});
|
|
185
|
+
// Any add with same dedup ID within 60s is ignored
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Manual Rate Limiting
|
|
189
|
+
|
|
190
|
+
Handle external API rate limits (e.g., 429 responses):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { Worker } from "bullmq";
|
|
194
|
+
|
|
195
|
+
const worker = new Worker(
|
|
196
|
+
"api-calls",
|
|
197
|
+
async () => {
|
|
198
|
+
const [isRateLimited, duration] = await callExternalApi();
|
|
199
|
+
if (isRateLimited) {
|
|
200
|
+
await worker.rateLimit(duration);
|
|
201
|
+
throw Worker.RateLimitError();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
connection,
|
|
206
|
+
limiter: { max: 1, duration: 500 }, // REQUIRED: must set limiter for rateLimit to work
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Static Rate Limiting
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const worker = new Worker("painter", async (job) => paintCar(job), {
|
|
215
|
+
limiter: {
|
|
216
|
+
max: 10, // max 10 jobs
|
|
217
|
+
duration: 1000, // per 1 second
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The rate limiter is **global** across all workers for the queue.
|
|
223
|
+
|
|
224
|
+
## Global Concurrency
|
|
225
|
+
|
|
226
|
+
Limit to 1 job processing at a time across all workers:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
await queue.setGlobalConcurrency(1);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Failing Fast When Redis Is Down
|
|
233
|
+
|
|
234
|
+
For producer services (HTTP endpoints), disable offline queue so calls fail immediately:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { Redis } from "ioredis";
|
|
238
|
+
|
|
239
|
+
const connection = new Redis({
|
|
240
|
+
maxRetriesPerRequest: 1,
|
|
241
|
+
enableOfflineQueue: false,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const queue = new Queue("q", { connection });
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await queue.add("job", data);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
// Redis is down — return 503 to the client
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Redis Cluster
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { Cluster } from "ioredis";
|
|
257
|
+
|
|
258
|
+
const connection = new Cluster(
|
|
259
|
+
[
|
|
260
|
+
{ host: "node1", port: 6380 },
|
|
261
|
+
{ host: "node2", port: 6380 },
|
|
262
|
+
],
|
|
263
|
+
{
|
|
264
|
+
natMap: {
|
|
265
|
+
/* ... */
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const queue = new Queue("q", { connection, prefix: "{myprefix}" });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use `{hashTag}` prefix to ensure all keys for a queue land on the same slot.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Production Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Redis Configuration](#redis-configuration)
|
|
6
|
+
- [Connection Strategy](#connection-strategy)
|
|
7
|
+
- [Graceful Shutdown](#graceful-shutdown)
|
|
8
|
+
- [Retry Strategies](#retry-strategies)
|
|
9
|
+
- [Stalled Jobs](#stalled-jobs)
|
|
10
|
+
- [Sandboxed Processors](#sandboxed-processors)
|
|
11
|
+
- [Concurrency](#concurrency)
|
|
12
|
+
- [Auto-Job Removal](#auto-job-removal)
|
|
13
|
+
- [Error Handling](#error-handling)
|
|
14
|
+
- [Monitoring with Prometheus](#monitoring-with-prometheus)
|
|
15
|
+
- [Data Security](#data-security)
|
|
16
|
+
- [Troubleshooting Checklist](#troubleshooting-checklist)
|
|
17
|
+
|
|
18
|
+
## Redis Configuration
|
|
19
|
+
|
|
20
|
+
### Persistence
|
|
21
|
+
|
|
22
|
+
Enable AOF (Append Only File) for durability. 1-second fsync is sufficient for most apps:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
# redis.conf
|
|
26
|
+
appendonly yes
|
|
27
|
+
appendfsync everysec
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Memory Policy
|
|
31
|
+
|
|
32
|
+
**Critical:** BullMQ will malfunction if Redis evicts keys.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
maxmemory-policy noeviction
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Connection Strategy
|
|
39
|
+
|
|
40
|
+
Different needs for producers vs consumers:
|
|
41
|
+
|
|
42
|
+
### Producers (Queue class) — fail fast
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Redis } from "ioredis";
|
|
46
|
+
|
|
47
|
+
const producerConn = new Redis({
|
|
48
|
+
host: "redis.example.com",
|
|
49
|
+
port: 6379,
|
|
50
|
+
maxRetriesPerRequest: 1, // fail quickly on disconnect
|
|
51
|
+
enableOfflineQueue: false, // don't queue commands when offline
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const queue = new Queue("q", { connection: producerConn });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Consumers (Worker class) — wait indefinitely
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const workerConn = new Redis({
|
|
61
|
+
host: "redis.example.com",
|
|
62
|
+
port: 6379,
|
|
63
|
+
maxRetriesPerRequest: null, // REQUIRED for workers — retry forever
|
|
64
|
+
// enableOfflineQueue: true // default, keeps queuing commands
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const worker = new Worker("q", processor, { connection: workerConn });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Retry Strategy
|
|
71
|
+
|
|
72
|
+
BullMQ default (for internally created connections):
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
retryStrategy: (times) => Math.max(Math.min(Math.exp(times), 20000), 1000);
|
|
76
|
+
// Exponential backoff: min 1s, max 20s
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Graceful Shutdown
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const gracefulShutdown = async (signal: string) => {
|
|
83
|
+
console.log(`Received ${signal}, closing worker...`);
|
|
84
|
+
await worker.close();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
89
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If the worker doesn't close before the process is killed, in-progress jobs become **stalled** and are re-processed after ~30 seconds by another worker.
|
|
93
|
+
|
|
94
|
+
## Retry Strategies
|
|
95
|
+
|
|
96
|
+
### Fixed Backoff
|
|
97
|
+
|
|
98
|
+
Retry after a constant delay:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await queue.add("job", data, {
|
|
102
|
+
attempts: 3,
|
|
103
|
+
backoff: { type: "fixed", delay: 1000 }, // retry every 1s
|
|
104
|
+
// With jitter (0-1, randomizes between delay*jitter and delay):
|
|
105
|
+
// backoff: { type: "fixed", delay: 1000, jitter: 0.5 },
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Exponential Backoff
|
|
110
|
+
|
|
111
|
+
Retry after `2^(attempts-1) * delay` ms:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await queue.add("job", data, {
|
|
115
|
+
attempts: 5,
|
|
116
|
+
backoff: { type: "exponential", delay: 1000 },
|
|
117
|
+
// Attempt 1: 1s, Attempt 2: 2s, Attempt 3: 4s, Attempt 4: 8s, Attempt 5: 16s
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Backoff
|
|
122
|
+
|
|
123
|
+
Define on the worker:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const worker = new Worker("q", processor, {
|
|
127
|
+
settings: {
|
|
128
|
+
backoffStrategy: (attemptsMade, type, err, job) => {
|
|
129
|
+
if (type === "linear") return attemptsMade * 1000;
|
|
130
|
+
if (type === "custom-exp") return Math.pow(2, attemptsMade) * 500;
|
|
131
|
+
throw new Error("unknown type");
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Use in job options
|
|
137
|
+
await queue.add("job", data, {
|
|
138
|
+
attempts: 5,
|
|
139
|
+
backoff: { type: "linear" },
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Special return values from `backoffStrategy`:**
|
|
144
|
+
|
|
145
|
+
- `0` → move to end of waiting list (or back to prioritized if priority > 0)
|
|
146
|
+
- `-1` → don't retry, move to failed immediately
|
|
147
|
+
|
|
148
|
+
### Default Job Options
|
|
149
|
+
|
|
150
|
+
Set retry defaults for all jobs in a queue:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const queue = new Queue("q", {
|
|
154
|
+
defaultJobOptions: {
|
|
155
|
+
attempts: 3,
|
|
156
|
+
backoff: { type: "exponential", delay: 1000 },
|
|
157
|
+
removeOnComplete: { count: 200 },
|
|
158
|
+
removeOnFail: { count: 1000 },
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Stalled Jobs
|
|
164
|
+
|
|
165
|
+
A job becomes stalled when the worker holding it doesn't renew its lock within `stalledInterval` (default: 30s). Causes:
|
|
166
|
+
|
|
167
|
+
- CPU-intensive synchronous code blocking the event loop
|
|
168
|
+
- Process crash or kill without graceful shutdown
|
|
169
|
+
|
|
170
|
+
Stalled jobs are automatically moved back to waiting. After `maxStalledCount` (default: 1), they move to failed.
|
|
171
|
+
|
|
172
|
+
**Prevention:**
|
|
173
|
+
|
|
174
|
+
- Avoid blocking the event loop — use `async`/`await`, break CPU work into chunks
|
|
175
|
+
- Use sandboxed processors for CPU-intensive tasks
|
|
176
|
+
- Increase `stalledInterval` if jobs legitimately need long uninterrupted processing
|
|
177
|
+
|
|
178
|
+
## Sandboxed Processors
|
|
179
|
+
|
|
180
|
+
Run processors in separate child processes for CPU-intensive work:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// processor.ts (separate file)
|
|
184
|
+
import { SandboxedJob } from "bullmq";
|
|
185
|
+
|
|
186
|
+
export default async function (job: SandboxedJob) {
|
|
187
|
+
// CPU-intensive work — runs in a child process
|
|
188
|
+
return heavyComputation(job.data);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// main.ts
|
|
192
|
+
const worker = new Worker("q", "./processor.ts", { connection });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Benefits: isolates CPU-heavy work, prevents stalled jobs, true parallelism with `concurrency > 1`.
|
|
196
|
+
|
|
197
|
+
## Concurrency
|
|
198
|
+
|
|
199
|
+
### Local Concurrency (single worker)
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const worker = new Worker("q", processor, { concurrency: 50 });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Only effective for I/O-bound (async) work. For CPU-bound work, use sandboxed processors.
|
|
206
|
+
|
|
207
|
+
### Multiple Workers (recommended for high availability)
|
|
208
|
+
|
|
209
|
+
Run workers across multiple processes/machines. Combine with local concurrency:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// Each machine runs:
|
|
213
|
+
const worker = new Worker("q", processor, { concurrency: 10 });
|
|
214
|
+
// 5 machines × 10 concurrency = 50 jobs processed concurrently
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Global Concurrency
|
|
218
|
+
|
|
219
|
+
Limit total concurrent processing across ALL workers:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
await queue.setGlobalConcurrency(1); // only 1 job at a time, globally
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Global Rate Limit
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const queue = new Queue("q");
|
|
229
|
+
// Must configure on the queue, not the worker
|
|
230
|
+
await queue.setGlobalConcurrency(5); // combined with worker limiter
|
|
231
|
+
|
|
232
|
+
// Check if rate limited
|
|
233
|
+
const ttl = await queue.getRateLimitTtl(100);
|
|
234
|
+
if (ttl > 0) console.log("Queue is rate limited");
|
|
235
|
+
|
|
236
|
+
// Reset rate limit
|
|
237
|
+
await queue.removeRateLimitKey();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Auto-Job Removal
|
|
241
|
+
|
|
242
|
+
By default, completed and failed jobs are kept forever. Always configure removal:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const queue = new Queue("q", {
|
|
246
|
+
defaultJobOptions: {
|
|
247
|
+
removeOnComplete: { count: 100, age: 3600 }, // keep 100 or 1 hour
|
|
248
|
+
removeOnFail: { count: 5000 }, // keep last 5000 failures
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Error Handling
|
|
254
|
+
|
|
255
|
+
Always attach error handlers to prevent unhandled exceptions:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
worker.on("error", (err) => {
|
|
259
|
+
logger.error(err, "Worker error");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
queue.on("error", (err) => {
|
|
263
|
+
logger.error(err, "Queue error");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Global safety net
|
|
267
|
+
process.on("uncaughtException", (err) => {
|
|
268
|
+
logger.error(err, "Uncaught exception");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
272
|
+
logger.error({ promise, reason }, "Unhandled rejection");
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Important:** processor functions must throw `Error` objects (not strings or other types).
|
|
277
|
+
|
|
278
|
+
## Monitoring with Prometheus
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { Queue, MetricsTime } from "bullmq";
|
|
282
|
+
|
|
283
|
+
const queue = new Queue("q", {
|
|
284
|
+
metrics: {
|
|
285
|
+
maxDataPoints: MetricsTime.ONE_WEEK * 2, // collect 2 weeks of data
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Retrieve metrics
|
|
290
|
+
const completedMetrics = await queue.getMetrics("completed");
|
|
291
|
+
// { meta: { count, prevCount }, data: number[] }
|
|
292
|
+
|
|
293
|
+
const failedMetrics = await queue.getMetrics("failed");
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Data Security
|
|
297
|
+
|
|
298
|
+
Job data is stored in **plain text** in Redis. Never store sensitive data (passwords, tokens, PII) in job payloads. If unavoidable, encrypt sensitive fields before adding to the queue.
|
|
299
|
+
|
|
300
|
+
## Troubleshooting Checklist
|
|
301
|
+
|
|
302
|
+
1. **Jobs not processing?** Check worker is connected to the same queue name with matching `prefix`.
|
|
303
|
+
2. **Stalled jobs?** CPU blocking the event loop — use sandboxed processors or break up work.
|
|
304
|
+
3. **Missing error handler?** Worker may silently stop processing — always attach `worker.on("error", ...)`.
|
|
305
|
+
4. **Redis memory growing?** Configure `removeOnComplete`/`removeOnFail`, check `maxmemory-policy`.
|
|
306
|
+
5. **Delayed jobs not firing?** Ensure Redis is reachable and no clock skew issues.
|
|
307
|
+
6. **Rate limiting not working?** `limiter` must be set on the worker options for `rateLimit()` to function.
|
|
308
|
+
7. **ioredis keyPrefix?** Don't use it — use BullMQ's `prefix` option instead.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: composition-patterns
|
|
3
|
+
description: React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Composition Patterns
|
|
7
|
+
|
|
8
|
+
Composition patterns for building flexible, maintainable React components. Avoid
|
|
9
|
+
boolean prop proliferation by using compound components, lifting state, and
|
|
10
|
+
composing internals. These patterns make codebases easier for both humans and AI
|
|
11
|
+
agents to work with as they scale.
|
|
12
|
+
|
|
13
|
+
## When NOT to Use
|
|
14
|
+
|
|
15
|
+
Skip these patterns when: fewer than 3 props, simple variants, or single-use components.
|
|
16
|
+
|
|
17
|
+
## When to Apply
|
|
18
|
+
|
|
19
|
+
Reference these guidelines when:
|
|
20
|
+
|
|
21
|
+
- Refactoring components with many boolean props
|
|
22
|
+
- Building reusable component libraries
|
|
23
|
+
- Designing flexible component APIs
|
|
24
|
+
- Reviewing component architecture
|
|
25
|
+
- Working with compound components or context providers
|
|
26
|
+
|
|
27
|
+
## Rule Categories by Priority
|
|
28
|
+
|
|
29
|
+
| Priority | Category | Impact | Prefix |
|
|
30
|
+
| -------- | ----------------------- | ------ | --------------- |
|
|
31
|
+
| 1 | Component Architecture | HIGH | `architecture-` |
|
|
32
|
+
| 2 | State Management | MEDIUM | `state-` |
|
|
33
|
+
| 3 | Implementation Patterns | MEDIUM | `patterns-` |
|
|
34
|
+
| 4 | React 19 APIs | MEDIUM | `react19-` |
|
|
35
|
+
|
|
36
|
+
## Quick Reference
|
|
37
|
+
|
|
38
|
+
### 1. Component Architecture (HIGH)
|
|
39
|
+
|
|
40
|
+
- **Avoid boolean props** — Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize behavior. Each boolean doubles possible states. Use composition instead — see [references/architecture-avoid-boolean-props.md](references/architecture-avoid-boolean-props.md)
|
|
41
|
+
- **Compound components** — Structure complex components with shared context so each subcomponent accesses state via context, not props — see [references/architecture-compound-components.md](references/architecture-compound-components.md)
|
|
42
|
+
|
|
43
|
+
### 2. State Management (MEDIUM)
|
|
44
|
+
|
|
45
|
+
- **Decouple implementation** — Provider is the only place that knows how state is managed — see [references/state-decouple-implementation.md](references/state-decouple-implementation.md)
|
|
46
|
+
- **Context interface** — Define generic interface with `state`, `actions`, `meta` for dependency injection — see [references/state-context-interface.md](references/state-context-interface.md)
|
|
47
|
+
- **Lift state** — Move state into provider components for sibling access — see [references/state-lift-state.md](references/state-lift-state.md)
|
|
48
|
+
|
|
49
|
+
### 3. Implementation Patterns (MEDIUM)
|
|
50
|
+
|
|
51
|
+
- **Explicit variants** — Create explicit variant components instead of boolean modes — see [references/patterns-explicit-variants.md](references/patterns-explicit-variants.md)
|
|
52
|
+
- **Children over render props** — Use `children` for composition instead of `renderX` props — see [references/patterns-children-over-render-props.md](references/patterns-children-over-render-props.md)
|
|
53
|
+
|
|
54
|
+
### 4. React 19 APIs (MEDIUM)
|
|
55
|
+
|
|
56
|
+
> **React 19+ only.** Skip this section if using React 18 or earlier.
|
|
57
|
+
|
|
58
|
+
- **No forwardRef** — Don't use `forwardRef`; pass `ref` as a regular prop. Use `use()` instead of `useContext()` — see [references/react19-no-forwardref.md](references/react19-no-forwardref.md)
|