@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-workers
|
|
3
|
+
description: Web Worker patterns for shared workers, service workers, Comlink RPC, transferable objects, and WebAssembly integration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Web Worker Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use Web Workers when you need to run CPU-intensive tasks without blocking the main thread. This includes image processing, data parsing, cryptographic operations, sorting large datasets, and running WebAssembly modules. Workers keep the UI responsive by moving computation off the main thread. Use Comlink to simplify the messaging API, and transferable objects to avoid expensive data copies.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Basic Dedicated Worker
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/workers/data-processor.worker.ts
|
|
17
|
+
interface ProcessRequest {
|
|
18
|
+
type: 'sort' | 'filter' | 'aggregate';
|
|
19
|
+
data: number[];
|
|
20
|
+
options?: { ascending?: boolean; threshold?: number };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
self.addEventListener('message', (event: MessageEvent<ProcessRequest>) => {
|
|
24
|
+
const { type, data, options } = event.data;
|
|
25
|
+
|
|
26
|
+
let result: unknown;
|
|
27
|
+
|
|
28
|
+
switch (type) {
|
|
29
|
+
case 'sort':
|
|
30
|
+
result = [...data].sort((a, b) => options?.ascending ? a - b : b - a);
|
|
31
|
+
break;
|
|
32
|
+
case 'filter':
|
|
33
|
+
result = data.filter((n) => n >= (options?.threshold ?? 0));
|
|
34
|
+
break;
|
|
35
|
+
case 'aggregate':
|
|
36
|
+
result = {
|
|
37
|
+
sum: data.reduce((a, b) => a + b, 0),
|
|
38
|
+
avg: data.reduce((a, b) => a + b, 0) / data.length,
|
|
39
|
+
min: Math.min(...data),
|
|
40
|
+
max: Math.max(...data),
|
|
41
|
+
};
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self.postMessage({ type, result });
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// src/use-worker.ts
|
|
51
|
+
export function createDataProcessor() {
|
|
52
|
+
const worker = new Worker(
|
|
53
|
+
new URL('./workers/data-processor.worker.ts', import.meta.url),
|
|
54
|
+
{ type: 'module' }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
process(request: ProcessRequest): Promise<unknown> {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const handler = (event: MessageEvent) => {
|
|
61
|
+
if (event.data.type === request.type) {
|
|
62
|
+
worker.removeEventListener('message', handler);
|
|
63
|
+
resolve(event.data.result);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
worker.addEventListener('message', handler);
|
|
67
|
+
worker.postMessage(request);
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
terminate: () => worker.terminate(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Comlink RPC (Simplified Messaging)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// src/workers/image-processor.worker.ts
|
|
79
|
+
import * as Comlink from 'comlink';
|
|
80
|
+
|
|
81
|
+
class ImageProcessor {
|
|
82
|
+
async resize(imageData: ImageData, width: number, height: number): Promise<ImageData> {
|
|
83
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
84
|
+
const ctx = canvas.getContext('2d')!;
|
|
85
|
+
const bitmap = await createImageBitmap(imageData);
|
|
86
|
+
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
87
|
+
return ctx.getImageData(0, 0, width, height);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async grayscale(imageData: ImageData): Promise<ImageData> {
|
|
91
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
92
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
93
|
+
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
94
|
+
data[i] = data[i + 1] = data[i + 2] = avg;
|
|
95
|
+
}
|
|
96
|
+
return new ImageData(data, imageData.width, imageData.height);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async blur(imageData: ImageData, radius: number): Promise<ImageData> {
|
|
100
|
+
const { width, height, data } = imageData;
|
|
101
|
+
const output = new Uint8ClampedArray(data);
|
|
102
|
+
// Simple box blur
|
|
103
|
+
for (let y = radius; y < height - radius; y++) {
|
|
104
|
+
for (let x = radius; x < width - radius; x++) {
|
|
105
|
+
for (let c = 0; c < 3; c++) {
|
|
106
|
+
let sum = 0, count = 0;
|
|
107
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
108
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
109
|
+
sum += data[((y + dy) * width + (x + dx)) * 4 + c];
|
|
110
|
+
count++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
output[(y * width + x) * 4 + c] = sum / count;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return new ImageData(output, width, height);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Comlink.expose(new ImageProcessor());
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/image-client.ts
|
|
126
|
+
import * as Comlink from 'comlink';
|
|
127
|
+
|
|
128
|
+
type ImageProcessor = {
|
|
129
|
+
resize(data: ImageData, w: number, h: number): Promise<ImageData>;
|
|
130
|
+
grayscale(data: ImageData): Promise<ImageData>;
|
|
131
|
+
blur(data: ImageData, radius: number): Promise<ImageData>;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const worker = new Worker(
|
|
135
|
+
new URL('./workers/image-processor.worker.ts', import.meta.url),
|
|
136
|
+
{ type: 'module' }
|
|
137
|
+
);
|
|
138
|
+
const processor = Comlink.wrap<ImageProcessor>(worker);
|
|
139
|
+
|
|
140
|
+
// Usage — call worker methods like local async functions
|
|
141
|
+
const resized = await processor.resize(imageData, 800, 600);
|
|
142
|
+
const gray = await processor.grayscale(imageData);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Transferable Objects (Zero-Copy)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// src/workers/transfer-worker.ts
|
|
149
|
+
self.addEventListener('message', (event: MessageEvent) => {
|
|
150
|
+
const buffer: ArrayBuffer = event.data;
|
|
151
|
+
const view = new Float64Array(buffer);
|
|
152
|
+
|
|
153
|
+
// Process in-place
|
|
154
|
+
for (let i = 0; i < view.length; i++) {
|
|
155
|
+
view[i] = Math.sqrt(view[i]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Transfer back — zero copy
|
|
159
|
+
self.postMessage(buffer, [buffer]);
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Main thread — transfer ownership to worker
|
|
165
|
+
const data = new Float64Array(1_000_000);
|
|
166
|
+
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 1000;
|
|
167
|
+
|
|
168
|
+
// Transfer ownership — main thread loses access, no copy made
|
|
169
|
+
worker.postMessage(data.buffer, [data.buffer]);
|
|
170
|
+
// data.buffer.byteLength === 0 after transfer
|
|
171
|
+
|
|
172
|
+
worker.addEventListener('message', (event) => {
|
|
173
|
+
const result = new Float64Array(event.data);
|
|
174
|
+
console.log('Processed:', result.length, 'items');
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Shared Worker (Multiple Tabs)
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// src/workers/shared-state.worker.ts
|
|
182
|
+
const connections: MessagePort[] = [];
|
|
183
|
+
let sharedState: Record<string, unknown> = {};
|
|
184
|
+
|
|
185
|
+
self.addEventListener('connect', (event: MessageEvent) => {
|
|
186
|
+
const port = (event as any).ports[0] as MessagePort;
|
|
187
|
+
connections.push(port);
|
|
188
|
+
|
|
189
|
+
port.addEventListener('message', (msg: MessageEvent) => {
|
|
190
|
+
const { action, key, value } = msg.data;
|
|
191
|
+
|
|
192
|
+
if (action === 'set') {
|
|
193
|
+
sharedState[key] = value;
|
|
194
|
+
// Broadcast to all connected tabs
|
|
195
|
+
connections.forEach((p) => p.postMessage({ type: 'update', state: sharedState }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (action === 'get') {
|
|
199
|
+
port.postMessage({ type: 'state', state: sharedState });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
port.start();
|
|
204
|
+
port.postMessage({ type: 'state', state: sharedState });
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Main thread — connect to shared worker
|
|
210
|
+
const shared = new SharedWorker(
|
|
211
|
+
new URL('./workers/shared-state.worker.ts', import.meta.url),
|
|
212
|
+
{ type: 'module' }
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
shared.port.addEventListener('message', (event) => {
|
|
216
|
+
if (event.data.type === 'update') {
|
|
217
|
+
console.log('State updated across tabs:', event.data.state);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
shared.port.start();
|
|
221
|
+
|
|
222
|
+
// Set value (broadcasts to all tabs)
|
|
223
|
+
shared.port.postMessage({ action: 'set', key: 'user', value: { name: 'Alice' } });
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### WASM Integration in Worker
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// src/workers/wasm-worker.ts
|
|
230
|
+
import * as Comlink from 'comlink';
|
|
231
|
+
|
|
232
|
+
let wasmInstance: WebAssembly.Instance | null = null;
|
|
233
|
+
|
|
234
|
+
async function initWasm(): Promise<void> {
|
|
235
|
+
if (wasmInstance) return;
|
|
236
|
+
const response = await fetch('/compute.wasm');
|
|
237
|
+
const { instance } = await WebAssembly.instantiateStreaming(response);
|
|
238
|
+
wasmInstance = instance;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const wasmApi = {
|
|
242
|
+
async fibonacci(n: number): Promise<number> {
|
|
243
|
+
await initWasm();
|
|
244
|
+
return (wasmInstance!.exports.fibonacci as (n: number) => number)(n);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
async processBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
|
|
248
|
+
await initWasm();
|
|
249
|
+
const memory = wasmInstance!.exports.memory as WebAssembly.Memory;
|
|
250
|
+
const alloc = wasmInstance!.exports.alloc as (size: number) => number;
|
|
251
|
+
const process = wasmInstance!.exports.process as (ptr: number, len: number) => number;
|
|
252
|
+
|
|
253
|
+
const inputPtr = alloc(data.byteLength);
|
|
254
|
+
new Uint8Array(memory.buffer, inputPtr, data.byteLength).set(new Uint8Array(data));
|
|
255
|
+
|
|
256
|
+
const outputPtr = process(inputPtr, data.byteLength);
|
|
257
|
+
const output = new Uint8Array(memory.buffer, outputPtr, data.byteLength).slice();
|
|
258
|
+
|
|
259
|
+
return output.buffer;
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
Comlink.expose(wasmApi);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Worker Pool
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// src/worker-pool.ts
|
|
270
|
+
export class WorkerPool<T> {
|
|
271
|
+
private workers: Worker[] = [];
|
|
272
|
+
private queue: Array<{ resolve: (v: T) => void; args: unknown }> = [];
|
|
273
|
+
private busy = new Set<Worker>();
|
|
274
|
+
|
|
275
|
+
constructor(private workerUrl: URL, private size: number = navigator.hardwareConcurrency) {
|
|
276
|
+
for (let i = 0; i < this.size; i++) {
|
|
277
|
+
this.workers.push(new Worker(workerUrl, { type: 'module' }));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async execute(args: unknown): Promise<T> {
|
|
282
|
+
const available = this.workers.find((w) => !this.busy.has(w));
|
|
283
|
+
if (available) return this.dispatch(available, args);
|
|
284
|
+
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
this.queue.push({ resolve, args });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private dispatch(worker: Worker, args: unknown): Promise<T> {
|
|
291
|
+
this.busy.add(worker);
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
worker.onmessage = (event: MessageEvent<T>) => {
|
|
294
|
+
this.busy.delete(worker);
|
|
295
|
+
resolve(event.data);
|
|
296
|
+
const next = this.queue.shift();
|
|
297
|
+
if (next) this.dispatch(worker, next.args).then(next.resolve);
|
|
298
|
+
};
|
|
299
|
+
worker.postMessage(args);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
terminate() {
|
|
304
|
+
this.workers.forEach((w) => w.terminate());
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Examples
|
|
310
|
+
|
|
311
|
+
| Pattern | Overhead | Use Case |
|
|
312
|
+
|---------|----------|----------|
|
|
313
|
+
| Dedicated Worker | ~1ms message | Image processing, sorting |
|
|
314
|
+
| Shared Worker | ~1ms message | Cross-tab state sync |
|
|
315
|
+
| Comlink | ~2ms message | Complex API, many methods |
|
|
316
|
+
| Transferable | Zero-copy | Large ArrayBuffer operations |
|
|
317
|
+
| Worker Pool | Pool management | Parallel batch processing |
|
|
318
|
+
| WASM in Worker | Init once | Heavy compute (crypto, codec) |
|
|
319
|
+
|
|
320
|
+
## Checklist
|
|
321
|
+
- [ ] Workers created with `new URL()` for bundler compatibility
|
|
322
|
+
- [ ] Comlink used for complex worker APIs to avoid manual message handling
|
|
323
|
+
- [ ] Large buffers transferred, not copied, using `[buffer]` transfer list
|
|
324
|
+
- [ ] Worker pool size matches `navigator.hardwareConcurrency`
|
|
325
|
+
- [ ] Workers terminated when no longer needed to free resources
|
|
326
|
+
- [ ] Error handler (`onerror`) set on every worker instance
|
|
327
|
+
- [ ] Feature detection before using SharedWorker or WebAssembly
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhooks-patterns
|
|
3
|
+
description: Webhook patterns with signature verification, retry logic, idempotency keys, event ordering, and testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Webhook Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when building webhook consumers (receiving events from Stripe, GitHub, etc.) or webhook producers (sending events to your users' endpoints). Webhooks are HTTP callbacks -- the sender POSTs to your URL when an event occurs. The key challenges are verification, reliability, idempotency, and ordering.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Receiving Webhooks -- Signature Verification
|
|
14
|
+
|
|
15
|
+
Always verify the webhook signature. Never trust the payload without it:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import crypto from "crypto";
|
|
19
|
+
import express from "express";
|
|
20
|
+
|
|
21
|
+
// Stripe signature verification
|
|
22
|
+
app.post(
|
|
23
|
+
"/webhooks/stripe",
|
|
24
|
+
express.raw({ type: "application/json" }),
|
|
25
|
+
(req, res) => {
|
|
26
|
+
const sig = req.headers["stripe-signature"] as string;
|
|
27
|
+
try {
|
|
28
|
+
const event = stripe.webhooks.constructEvent(
|
|
29
|
+
req.body,
|
|
30
|
+
sig,
|
|
31
|
+
process.env.STRIPE_WEBHOOK_SECRET!
|
|
32
|
+
);
|
|
33
|
+
processEvent(event);
|
|
34
|
+
res.json({ received: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
res.status(400).send("Invalid signature");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Generic HMAC-SHA256 verification (GitHub, custom webhooks)
|
|
42
|
+
function verifyWebhookSignature(
|
|
43
|
+
payload: Buffer,
|
|
44
|
+
signature: string,
|
|
45
|
+
secret: string
|
|
46
|
+
): boolean {
|
|
47
|
+
const expected = crypto
|
|
48
|
+
.createHmac("sha256", secret)
|
|
49
|
+
.update(payload)
|
|
50
|
+
.digest("hex");
|
|
51
|
+
const received = signature.replace("sha256=", "");
|
|
52
|
+
return crypto.timingSafeEqual(
|
|
53
|
+
Buffer.from(expected, "hex"),
|
|
54
|
+
Buffer.from(received, "hex")
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
app.post(
|
|
59
|
+
"/webhooks/github",
|
|
60
|
+
express.raw({ type: "application/json" }),
|
|
61
|
+
(req, res) => {
|
|
62
|
+
const sig = req.headers["x-hub-signature-256"] as string;
|
|
63
|
+
if (!verifyWebhookSignature(req.body, sig, process.env.GITHUB_WEBHOOK_SECRET!)) {
|
|
64
|
+
return res.status(401).send("Invalid signature");
|
|
65
|
+
}
|
|
66
|
+
const event = JSON.parse(req.body.toString());
|
|
67
|
+
processGitHubEvent(event);
|
|
68
|
+
res.status(200).send("OK");
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Idempotent Processing
|
|
74
|
+
|
|
75
|
+
Webhooks may be delivered more than once. Make handlers safe to re-run:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
async function processWebhookIdempotent(
|
|
79
|
+
eventId: string,
|
|
80
|
+
handler: () => Promise<void>
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
// Atomic check-and-set in Redis
|
|
83
|
+
const acquired = await redis.set(
|
|
84
|
+
`webhook:processed:${eventId}`, "1", "NX", "EX", 86400
|
|
85
|
+
);
|
|
86
|
+
if (!acquired) {
|
|
87
|
+
logger.info({ eventId }, "Duplicate webhook, skipping");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await handler();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Remove the key so the webhook can be retried
|
|
95
|
+
await redis.del(`webhook:processed:${eventId}`);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Or use database constraints
|
|
101
|
+
await db.query(
|
|
102
|
+
`INSERT INTO webhook_events (event_id, type, processed_at)
|
|
103
|
+
VALUES ($1, $2, NOW())
|
|
104
|
+
ON CONFLICT (event_id) DO NOTHING`,
|
|
105
|
+
[event.id, event.type]
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Sending Webhooks -- Producer Side
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface WebhookDelivery {
|
|
113
|
+
id: string;
|
|
114
|
+
endpoint: string;
|
|
115
|
+
event: string;
|
|
116
|
+
payload: object;
|
|
117
|
+
attempt: number;
|
|
118
|
+
nextRetryAt: Date | null;
|
|
119
|
+
deliveredAt: Date | null;
|
|
120
|
+
statusCode: number | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function sendWebhook(
|
|
124
|
+
endpoint: string,
|
|
125
|
+
secret: string,
|
|
126
|
+
event: string,
|
|
127
|
+
payload: object
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const body = JSON.stringify(payload);
|
|
130
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
131
|
+
const signature = crypto
|
|
132
|
+
.createHmac("sha256", secret)
|
|
133
|
+
.update(`${timestamp}.${body}`)
|
|
134
|
+
.digest("hex");
|
|
135
|
+
|
|
136
|
+
const response = await fetch(endpoint, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
"X-Webhook-Signature": `t=${timestamp},v1=${signature}`,
|
|
141
|
+
"X-Webhook-Event": event,
|
|
142
|
+
"X-Webhook-ID": crypto.randomUUID(),
|
|
143
|
+
},
|
|
144
|
+
body,
|
|
145
|
+
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw new Error(`Webhook delivery failed: ${response.status}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Retry Logic with Exponential Backoff
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const RETRY_SCHEDULE = [
|
|
158
|
+
60, // 1 minute
|
|
159
|
+
300, // 5 minutes
|
|
160
|
+
1800, // 30 minutes
|
|
161
|
+
7200, // 2 hours
|
|
162
|
+
28800, // 8 hours
|
|
163
|
+
86400, // 24 hours
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
async function deliverWithRetry(delivery: WebhookDelivery): Promise<void> {
|
|
167
|
+
try {
|
|
168
|
+
await sendWebhook(
|
|
169
|
+
delivery.endpoint,
|
|
170
|
+
delivery.secret,
|
|
171
|
+
delivery.event,
|
|
172
|
+
delivery.payload
|
|
173
|
+
);
|
|
174
|
+
await db.webhookDeliveries.update(delivery.id, {
|
|
175
|
+
deliveredAt: new Date(),
|
|
176
|
+
statusCode: 200,
|
|
177
|
+
});
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const nextAttempt = delivery.attempt + 1;
|
|
180
|
+
if (nextAttempt >= RETRY_SCHEDULE.length) {
|
|
181
|
+
await db.webhookDeliveries.update(delivery.id, {
|
|
182
|
+
statusCode: 0,
|
|
183
|
+
failedPermanently: true,
|
|
184
|
+
});
|
|
185
|
+
logger.error({ deliveryId: delivery.id }, "Webhook permanently failed");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const delaySeconds = RETRY_SCHEDULE[nextAttempt];
|
|
190
|
+
await db.webhookDeliveries.update(delivery.id, {
|
|
191
|
+
attempt: nextAttempt,
|
|
192
|
+
nextRetryAt: new Date(Date.now() + delaySeconds * 1000),
|
|
193
|
+
lastError: (err as Error).message,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Event Ordering
|
|
200
|
+
|
|
201
|
+
Webhooks may arrive out of order. Handle this gracefully:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Include a timestamp or sequence number in the event
|
|
205
|
+
interface WebhookEvent {
|
|
206
|
+
id: string;
|
|
207
|
+
type: string;
|
|
208
|
+
createdAt: string; // ISO timestamp
|
|
209
|
+
data: Record<string, unknown>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// For order-sensitive events, check the timestamp
|
|
213
|
+
async function handleOrderStatusChange(event: WebhookEvent): Promise<void> {
|
|
214
|
+
const order = await db.orders.findById(event.data.orderId as string);
|
|
215
|
+
|
|
216
|
+
// Ignore events older than the last processed event
|
|
217
|
+
if (order.lastWebhookAt && new Date(event.createdAt) < order.lastWebhookAt) {
|
|
218
|
+
logger.warn({ eventId: event.id }, "Out-of-order webhook, skipping");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await db.orders.update(order.id, {
|
|
223
|
+
status: event.data.status as string,
|
|
224
|
+
lastWebhookAt: new Date(event.createdAt),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Testing Webhooks Locally
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Stripe CLI -- forward webhooks to localhost
|
|
233
|
+
stripe listen --forward-to localhost:3000/webhooks/stripe
|
|
234
|
+
|
|
235
|
+
# ngrok -- expose local server to the internet
|
|
236
|
+
ngrok http 3000
|
|
237
|
+
|
|
238
|
+
# Use the ngrok URL as your webhook endpoint in development
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Integration test for webhook handler
|
|
243
|
+
describe("POST /webhooks/stripe", () => {
|
|
244
|
+
it("processes checkout.session.completed", async () => {
|
|
245
|
+
const payload = { id: "evt_123", type: "checkout.session.completed", data: { /* ... */ } };
|
|
246
|
+
const body = JSON.stringify(payload);
|
|
247
|
+
const sig = stripe.webhooks.generateTestHeaderString({
|
|
248
|
+
payload: body,
|
|
249
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const res = await request(app)
|
|
253
|
+
.post("/webhooks/stripe")
|
|
254
|
+
.set("Content-Type", "application/json")
|
|
255
|
+
.set("stripe-signature", sig)
|
|
256
|
+
.send(body);
|
|
257
|
+
|
|
258
|
+
expect(res.status).toBe(200);
|
|
259
|
+
const order = await db.orders.findById(payload.data.object.metadata.orderId);
|
|
260
|
+
expect(order.status).toBe("fulfilled");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Examples
|
|
266
|
+
|
|
267
|
+
| Pattern | When | Result |
|
|
268
|
+
|---------|------|--------|
|
|
269
|
+
| HMAC signature | Every webhook | Prevents spoofed payloads |
|
|
270
|
+
| Idempotency key | Duplicate deliveries | Process each event exactly once |
|
|
271
|
+
| Retry with backoff | Network failures | Reliable delivery over 24 hours |
|
|
272
|
+
| Timestamp ordering | Status updates | Ignore stale events |
|
|
273
|
+
| Local forwarding | Development | Test webhooks without deploying |
|
|
274
|
+
|
|
275
|
+
## Checklist
|
|
276
|
+
- [ ] Webhook signatures verified on every incoming request
|
|
277
|
+
- [ ] Raw request body used for signature verification (not parsed JSON)
|
|
278
|
+
- [ ] Handlers are idempotent -- safe to process duplicate events
|
|
279
|
+
- [ ] Respond 200 quickly, process asynchronously if slow
|
|
280
|
+
- [ ] Retry schedule uses exponential backoff (1min to 24hrs)
|
|
281
|
+
- [ ] Failed deliveries logged with attempt count and error
|
|
282
|
+
- [ ] Out-of-order events handled via timestamp comparison
|
|
283
|
+
- [ ] Webhook endpoint tested with local forwarding (Stripe CLI, ngrok)
|