@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.ts"
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TypeScript Async Patterns
|
|
8
|
+
|
|
9
|
+
## Promise Basics
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// GOOD - explicit return type
|
|
13
|
+
async function fetchUser(id: string): Promise<User> {
|
|
14
|
+
const response = await fetch(`/api/users/${id}`);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Failed to fetch user: ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// GOOD - type the promise result
|
|
22
|
+
const userPromise: Promise<User> = fetchUser('123');
|
|
23
|
+
|
|
24
|
+
// BAD - untyped promise
|
|
25
|
+
const data = await fetch(url).then(r => r.json()); // any!
|
|
26
|
+
|
|
27
|
+
// GOOD - typed fetch
|
|
28
|
+
const data: User = await fetch(url).then(r => r.json() as Promise<User>);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Error Handling
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// GOOD - try/catch with typed error handling
|
|
35
|
+
async function fetchData<T>(url: string): Promise<T> {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new HttpError(response.status, response.statusText);
|
|
40
|
+
}
|
|
41
|
+
return await response.json();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof HttpError) {
|
|
44
|
+
// Handle HTTP errors specifically
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
if (error instanceof TypeError) {
|
|
48
|
+
// Network error
|
|
49
|
+
throw new NetworkError('Network request failed');
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Result pattern - avoid throwing for expected failures
|
|
56
|
+
type Result<T, E = Error> =
|
|
57
|
+
| { ok: true; value: T }
|
|
58
|
+
| { ok: false; error: E };
|
|
59
|
+
|
|
60
|
+
async function safeFetch<T>(url: string): Promise<Result<T>> {
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(url);
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
return { ok: false, error: new Error(`HTTP ${response.status}`) };
|
|
65
|
+
}
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
return { ok: true, value: data };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, error: error as Error };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Usage
|
|
74
|
+
const result = await safeFetch<User>('/api/users/1');
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
console.log(result.value.name);
|
|
77
|
+
} else {
|
|
78
|
+
console.error(result.error.message);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Parallel Execution
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// GOOD - parallel independent requests
|
|
86
|
+
async function fetchDashboard(userId: string): Promise<Dashboard> {
|
|
87
|
+
const [user, posts, notifications] = await Promise.all([
|
|
88
|
+
fetchUser(userId),
|
|
89
|
+
fetchPosts(userId),
|
|
90
|
+
fetchNotifications(userId),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
return { user, posts, notifications };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// GOOD - parallel with error handling
|
|
97
|
+
async function fetchAllUsers(ids: string[]): Promise<(User | null)[]> {
|
|
98
|
+
const results = await Promise.allSettled(
|
|
99
|
+
ids.map(id => fetchUser(id))
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return results.map(result =>
|
|
103
|
+
result.status === 'fulfilled' ? result.value : null
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// BAD - sequential when parallel is possible
|
|
108
|
+
async function fetchDashboardSlow(userId: string) {
|
|
109
|
+
const user = await fetchUser(userId);
|
|
110
|
+
const posts = await fetchPosts(userId); // Waits for user
|
|
111
|
+
const notifications = await fetchNotifications(userId); // Waits for posts
|
|
112
|
+
return { user, posts, notifications };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// GOOD - controlled concurrency
|
|
116
|
+
async function fetchWithLimit<T>(
|
|
117
|
+
items: string[],
|
|
118
|
+
fetcher: (id: string) => Promise<T>,
|
|
119
|
+
limit = 5
|
|
120
|
+
): Promise<T[]> {
|
|
121
|
+
const results: T[] = [];
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < items.length; i += limit) {
|
|
124
|
+
const batch = items.slice(i, i + limit);
|
|
125
|
+
const batchResults = await Promise.all(batch.map(fetcher));
|
|
126
|
+
results.push(...batchResults);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## AbortController & Cancellation
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// GOOD - cancellable fetch
|
|
137
|
+
async function fetchWithTimeout<T>(
|
|
138
|
+
url: string,
|
|
139
|
+
timeoutMs: number
|
|
140
|
+
): Promise<T> {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
146
|
+
return await response.json();
|
|
147
|
+
} finally {
|
|
148
|
+
clearTimeout(timeoutId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// GOOD - cancellable operation
|
|
153
|
+
async function searchUsers(
|
|
154
|
+
query: string,
|
|
155
|
+
signal?: AbortSignal
|
|
156
|
+
): Promise<User[]> {
|
|
157
|
+
const response = await fetch(`/api/users?q=${query}`, { signal });
|
|
158
|
+
|
|
159
|
+
if (signal?.aborted) {
|
|
160
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return response.json();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Usage in React
|
|
167
|
+
function useSearch(query: string) {
|
|
168
|
+
const [results, setResults] = useState<User[]>([]);
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
|
|
173
|
+
searchUsers(query, controller.signal)
|
|
174
|
+
.then(setResults)
|
|
175
|
+
.catch(error => {
|
|
176
|
+
if (error.name !== 'AbortError') {
|
|
177
|
+
console.error(error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return () => controller.abort();
|
|
182
|
+
}, [query]);
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Cancellation token pattern
|
|
188
|
+
class CancellationToken {
|
|
189
|
+
private controller = new AbortController();
|
|
190
|
+
|
|
191
|
+
get signal(): AbortSignal {
|
|
192
|
+
return this.controller.signal;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get isCancelled(): boolean {
|
|
196
|
+
return this.controller.signal.aborted;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cancel(): void {
|
|
200
|
+
this.controller.abort();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throwIfCancelled(): void {
|
|
204
|
+
if (this.isCancelled) {
|
|
205
|
+
throw new DOMException('Cancelled', 'AbortError');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Retry Logic
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface RetryOptions {
|
|
215
|
+
maxAttempts: number;
|
|
216
|
+
delayMs: number;
|
|
217
|
+
backoff?: 'linear' | 'exponential';
|
|
218
|
+
shouldRetry?: (error: Error) => boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function withRetry<T>(
|
|
222
|
+
fn: () => Promise<T>,
|
|
223
|
+
options: RetryOptions
|
|
224
|
+
): Promise<T> {
|
|
225
|
+
const { maxAttempts, delayMs, backoff = 'exponential', shouldRetry } = options;
|
|
226
|
+
|
|
227
|
+
let lastError: Error;
|
|
228
|
+
|
|
229
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
230
|
+
try {
|
|
231
|
+
return await fn();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
lastError = error as Error;
|
|
234
|
+
|
|
235
|
+
if (shouldRetry && !shouldRetry(lastError)) {
|
|
236
|
+
throw lastError;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (attempt === maxAttempts) {
|
|
240
|
+
throw lastError;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const delay = backoff === 'exponential'
|
|
244
|
+
? delayMs * Math.pow(2, attempt - 1)
|
|
245
|
+
: delayMs * attempt;
|
|
246
|
+
|
|
247
|
+
await sleep(delay);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
throw lastError!;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Usage
|
|
255
|
+
const user = await withRetry(
|
|
256
|
+
() => fetchUser('123'),
|
|
257
|
+
{
|
|
258
|
+
maxAttempts: 3,
|
|
259
|
+
delayMs: 1000,
|
|
260
|
+
backoff: 'exponential',
|
|
261
|
+
shouldRetry: (error) => error instanceof NetworkError,
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Debounce & Throttle
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Debounce - wait for pause in calls
|
|
270
|
+
function debounce<T extends (...args: any[]) => any>(
|
|
271
|
+
fn: T,
|
|
272
|
+
delayMs: number
|
|
273
|
+
): (...args: Parameters<T>) => void {
|
|
274
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
275
|
+
|
|
276
|
+
return (...args: Parameters<T>) => {
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
timeoutId = setTimeout(() => fn(...args), delayMs);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Debounce with promise
|
|
283
|
+
function debounceAsync<T extends (...args: any[]) => Promise<any>>(
|
|
284
|
+
fn: T,
|
|
285
|
+
delayMs: number
|
|
286
|
+
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
|
|
287
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
288
|
+
let pending: Promise<any> | null = null;
|
|
289
|
+
|
|
290
|
+
return (...args: Parameters<T>) => {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
|
|
293
|
+
pending = new Promise((resolve, reject) => {
|
|
294
|
+
timeoutId = setTimeout(async () => {
|
|
295
|
+
try {
|
|
296
|
+
resolve(await fn(...args));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
reject(error);
|
|
299
|
+
}
|
|
300
|
+
}, delayMs);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return pending;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Throttle - limit call frequency
|
|
308
|
+
function throttle<T extends (...args: any[]) => any>(
|
|
309
|
+
fn: T,
|
|
310
|
+
limitMs: number
|
|
311
|
+
): (...args: Parameters<T>) => void {
|
|
312
|
+
let lastRun = 0;
|
|
313
|
+
|
|
314
|
+
return (...args: Parameters<T>) => {
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
if (now - lastRun >= limitMs) {
|
|
317
|
+
lastRun = now;
|
|
318
|
+
fn(...args);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Async Iteration
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// Async generator
|
|
328
|
+
async function* fetchPages<T>(
|
|
329
|
+
baseUrl: string,
|
|
330
|
+
pageSize: number
|
|
331
|
+
): AsyncGenerator<T[]> {
|
|
332
|
+
let page = 0;
|
|
333
|
+
let hasMore = true;
|
|
334
|
+
|
|
335
|
+
while (hasMore) {
|
|
336
|
+
const response = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`);
|
|
337
|
+
const data = await response.json();
|
|
338
|
+
|
|
339
|
+
yield data.items;
|
|
340
|
+
|
|
341
|
+
hasMore = data.items.length === pageSize;
|
|
342
|
+
page++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Usage
|
|
347
|
+
for await (const users of fetchPages<User>('/api/users', 100)) {
|
|
348
|
+
console.log(`Fetched ${users.length} users`);
|
|
349
|
+
await processUsers(users);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Async iterable with cancellation
|
|
353
|
+
async function* fetchWithCancellation<T>(
|
|
354
|
+
urls: string[],
|
|
355
|
+
signal?: AbortSignal
|
|
356
|
+
): AsyncGenerator<T> {
|
|
357
|
+
for (const url of urls) {
|
|
358
|
+
signal?.throwIfAborted();
|
|
359
|
+
|
|
360
|
+
const response = await fetch(url, { signal });
|
|
361
|
+
yield await response.json();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Queue Pattern
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
class AsyncQueue<T> {
|
|
370
|
+
private queue: (() => Promise<T>)[] = [];
|
|
371
|
+
private processing = false;
|
|
372
|
+
private concurrency: number;
|
|
373
|
+
|
|
374
|
+
constructor(concurrency = 1) {
|
|
375
|
+
this.concurrency = concurrency;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async add(task: () => Promise<T>): Promise<T> {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
this.queue.push(async () => {
|
|
381
|
+
try {
|
|
382
|
+
resolve(await task());
|
|
383
|
+
} catch (error) {
|
|
384
|
+
reject(error);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
this.process();
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private async process(): Promise<void> {
|
|
392
|
+
if (this.processing) return;
|
|
393
|
+
this.processing = true;
|
|
394
|
+
|
|
395
|
+
while (this.queue.length > 0) {
|
|
396
|
+
const batch = this.queue.splice(0, this.concurrency);
|
|
397
|
+
await Promise.all(batch.map(task => task()));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.processing = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Usage
|
|
405
|
+
const queue = new AsyncQueue(3);
|
|
406
|
+
|
|
407
|
+
urls.forEach(url => {
|
|
408
|
+
queue.add(() => fetch(url).then(r => r.json()));
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Utilities
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// Sleep
|
|
416
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
417
|
+
|
|
418
|
+
// Timeout wrapper
|
|
419
|
+
function withTimeout<T>(
|
|
420
|
+
promise: Promise<T>,
|
|
421
|
+
timeoutMs: number,
|
|
422
|
+
message = 'Operation timed out'
|
|
423
|
+
): Promise<T> {
|
|
424
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
425
|
+
setTimeout(() => reject(new Error(message)), timeoutMs)
|
|
426
|
+
);
|
|
427
|
+
return Promise.race([promise, timeout]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// First successful
|
|
431
|
+
async function firstSuccess<T>(
|
|
432
|
+
promises: Promise<T>[]
|
|
433
|
+
): Promise<T> {
|
|
434
|
+
const errors: Error[] = [];
|
|
435
|
+
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
promises.forEach(promise => {
|
|
438
|
+
promise.then(resolve).catch(error => {
|
|
439
|
+
errors.push(error);
|
|
440
|
+
if (errors.length === promises.length) {
|
|
441
|
+
reject(new AggregateError(errors, 'All promises failed'));
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
```
|