@stacksjs/ts-cloud-core 0.1.1
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.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File hashing utilities for deployment optimization
|
|
3
|
+
* Fast hashing for detecting changed files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
export interface FileHash {
|
|
11
|
+
path: string
|
|
12
|
+
hash: string
|
|
13
|
+
size: number
|
|
14
|
+
mtime: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HashOptions {
|
|
18
|
+
algorithm?: 'md5' | 'sha1' | 'sha256'
|
|
19
|
+
chunkSize?: number
|
|
20
|
+
ignorePatterns?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hash a file using streaming for large files
|
|
25
|
+
*/
|
|
26
|
+
export async function hashFile(
|
|
27
|
+
filePath: string,
|
|
28
|
+
options: HashOptions = {},
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const algorithm = options.algorithm || 'sha256'
|
|
31
|
+
const chunkSize = options.chunkSize || 64 * 1024 // 64KB chunks
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const hash = createHash(algorithm)
|
|
35
|
+
const stream = fs.createReadStream(filePath, { highWaterMark: chunkSize })
|
|
36
|
+
|
|
37
|
+
stream.on('data', chunk => hash.update(chunk))
|
|
38
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
39
|
+
stream.on('error', reject)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hash a string
|
|
45
|
+
*/
|
|
46
|
+
export function hashString(content: string, algorithm: 'md5' | 'sha1' | 'sha256' = 'sha256'): string {
|
|
47
|
+
return createHash(algorithm).update(content).digest('hex')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hash a buffer
|
|
52
|
+
*/
|
|
53
|
+
export function hashBuffer(buffer: Buffer, algorithm: 'md5' | 'sha1' | 'sha256' = 'sha256'): string {
|
|
54
|
+
return createHash(algorithm).update(buffer).digest('hex')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hash all files in a directory
|
|
59
|
+
*/
|
|
60
|
+
export async function hashDirectory(
|
|
61
|
+
dirPath: string,
|
|
62
|
+
options: HashOptions = {},
|
|
63
|
+
): Promise<FileHash[]> {
|
|
64
|
+
const ignorePatterns = options.ignorePatterns || [
|
|
65
|
+
'node_modules',
|
|
66
|
+
'.git',
|
|
67
|
+
'dist',
|
|
68
|
+
'build',
|
|
69
|
+
'.ts-cloud',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const files: FileHash[] = []
|
|
73
|
+
|
|
74
|
+
async function walk(dir: string): Promise<void> {
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const fullPath = path.join(dir, entry.name)
|
|
79
|
+
const relativePath = path.relative(dirPath, fullPath)
|
|
80
|
+
|
|
81
|
+
// Skip ignored patterns
|
|
82
|
+
if (ignorePatterns.some(pattern => relativePath.includes(pattern))) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
await walk(fullPath)
|
|
88
|
+
}
|
|
89
|
+
else if (entry.isFile()) {
|
|
90
|
+
const stats = fs.statSync(fullPath)
|
|
91
|
+
const hash = await hashFile(fullPath, options)
|
|
92
|
+
|
|
93
|
+
files.push({
|
|
94
|
+
path: relativePath,
|
|
95
|
+
hash,
|
|
96
|
+
size: stats.size,
|
|
97
|
+
mtime: stats.mtimeMs,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await walk(dirPath)
|
|
104
|
+
|
|
105
|
+
return files
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a manifest hash from multiple file hashes
|
|
110
|
+
* Useful for detecting if any file in a directory has changed
|
|
111
|
+
*/
|
|
112
|
+
export function hashManifest(fileHashes: FileHash[]): string {
|
|
113
|
+
const sorted = [...fileHashes].sort((a, b) => a.path.localeCompare(b.path))
|
|
114
|
+
const content = sorted.map(f => `${f.path}:${f.hash}`).join('\n')
|
|
115
|
+
return hashString(content)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fast hash using file metadata (size + mtime)
|
|
120
|
+
* Much faster than content hash, but less reliable
|
|
121
|
+
* Use for quick change detection
|
|
122
|
+
*/
|
|
123
|
+
export function quickHash(filePath: string): string {
|
|
124
|
+
const stats = fs.statSync(filePath)
|
|
125
|
+
return hashString(`${filePath}:${stats.size}:${stats.mtimeMs}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compare two sets of file hashes to find changes
|
|
130
|
+
*/
|
|
131
|
+
export function findChangedFiles(
|
|
132
|
+
oldHashes: FileHash[],
|
|
133
|
+
newHashes: FileHash[],
|
|
134
|
+
): {
|
|
135
|
+
added: FileHash[]
|
|
136
|
+
modified: FileHash[]
|
|
137
|
+
deleted: FileHash[]
|
|
138
|
+
} {
|
|
139
|
+
const oldMap = new Map(oldHashes.map(f => [f.path, f]))
|
|
140
|
+
const newMap = new Map(newHashes.map(f => [f.path, f]))
|
|
141
|
+
|
|
142
|
+
const added: FileHash[] = []
|
|
143
|
+
const modified: FileHash[] = []
|
|
144
|
+
const deleted: FileHash[] = []
|
|
145
|
+
|
|
146
|
+
// Find added and modified
|
|
147
|
+
for (const [path, newFile] of newMap) {
|
|
148
|
+
const oldFile = oldMap.get(path)
|
|
149
|
+
|
|
150
|
+
if (!oldFile) {
|
|
151
|
+
added.push(newFile)
|
|
152
|
+
}
|
|
153
|
+
else if (oldFile.hash !== newFile.hash) {
|
|
154
|
+
modified.push(newFile)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Find deleted
|
|
159
|
+
for (const [path, oldFile] of oldMap) {
|
|
160
|
+
if (!newMap.has(path)) {
|
|
161
|
+
deleted.push(oldFile)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { added, modified, deleted }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cache for file hashes
|
|
170
|
+
*/
|
|
171
|
+
export class HashCache {
|
|
172
|
+
private cache: Map<string, { hash: string, mtime: number, size: number }>
|
|
173
|
+
|
|
174
|
+
constructor() {
|
|
175
|
+
this.cache = new Map()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get cached hash if file hasn't changed
|
|
180
|
+
*/
|
|
181
|
+
get(filePath: string): string | undefined {
|
|
182
|
+
const stats = fs.statSync(filePath)
|
|
183
|
+
const cached = this.cache.get(filePath)
|
|
184
|
+
|
|
185
|
+
if (cached && cached.mtime === stats.mtimeMs && cached.size === stats.size) {
|
|
186
|
+
return cached.hash
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cache a file hash
|
|
194
|
+
*/
|
|
195
|
+
set(filePath: string, hash: string): void {
|
|
196
|
+
const stats = fs.statSync(filePath)
|
|
197
|
+
|
|
198
|
+
this.cache.set(filePath, {
|
|
199
|
+
hash,
|
|
200
|
+
mtime: stats.mtimeMs,
|
|
201
|
+
size: stats.size,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get or compute hash
|
|
207
|
+
*/
|
|
208
|
+
async getOrCompute(filePath: string, options: HashOptions = {}): Promise<string> {
|
|
209
|
+
const cached = this.get(filePath)
|
|
210
|
+
|
|
211
|
+
if (cached) {
|
|
212
|
+
return cached
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const hash = await hashFile(filePath, options)
|
|
216
|
+
this.set(filePath, hash)
|
|
217
|
+
|
|
218
|
+
return hash
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Clear cache
|
|
223
|
+
*/
|
|
224
|
+
clear(): void {
|
|
225
|
+
this.cache.clear()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel execution utilities
|
|
3
|
+
* Optimize deployment performance with parallel operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ParallelOptions {
|
|
7
|
+
concurrency?: number
|
|
8
|
+
stopOnError?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ParallelResult<T> {
|
|
12
|
+
results: T[]
|
|
13
|
+
errors: Error[]
|
|
14
|
+
duration: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute tasks in parallel with configurable concurrency
|
|
19
|
+
*/
|
|
20
|
+
export async function parallel<T>(
|
|
21
|
+
tasks: (() => Promise<T>)[],
|
|
22
|
+
options: ParallelOptions = {},
|
|
23
|
+
): Promise<ParallelResult<T>> {
|
|
24
|
+
const concurrency = options.concurrency || 5
|
|
25
|
+
const stopOnError = options.stopOnError ?? true
|
|
26
|
+
|
|
27
|
+
const results: T[] = []
|
|
28
|
+
const errors: Error[] = []
|
|
29
|
+
const startTime = Date.now()
|
|
30
|
+
|
|
31
|
+
const queue = [...tasks]
|
|
32
|
+
const running: Promise<void>[] = []
|
|
33
|
+
|
|
34
|
+
async function runTask(task: () => Promise<T>, index: number): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
const result = await task()
|
|
37
|
+
results[index] = result
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
41
|
+
errors.push(err)
|
|
42
|
+
|
|
43
|
+
if (stopOnError) {
|
|
44
|
+
queue.length = 0 // Clear queue
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let index = 0
|
|
51
|
+
while (queue.length > 0 || running.length > 0) {
|
|
52
|
+
// Fill up to concurrency limit
|
|
53
|
+
while (running.length < concurrency && queue.length > 0) {
|
|
54
|
+
const task = queue.shift()!
|
|
55
|
+
const currentIndex = index++
|
|
56
|
+
const promise = runTask(task, currentIndex)
|
|
57
|
+
running.push(promise)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait for at least one task to complete
|
|
61
|
+
if (running.length > 0) {
|
|
62
|
+
await Promise.race(running)
|
|
63
|
+
|
|
64
|
+
// Remove completed tasks
|
|
65
|
+
for (let i = running.length - 1; i >= 0; i--) {
|
|
66
|
+
const settled = await Promise.race([
|
|
67
|
+
running[i].then(() => true),
|
|
68
|
+
Promise.resolve(false),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
if (settled) {
|
|
72
|
+
running.splice(i, 1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Stop if error occurred and stopOnError is true
|
|
78
|
+
if (stopOnError && errors.length > 0) {
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const duration = Date.now() - startTime
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
results: results.filter(r => r !== undefined),
|
|
87
|
+
errors,
|
|
88
|
+
duration,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Execute tasks in batches with controlled concurrency
|
|
94
|
+
*/
|
|
95
|
+
export async function batch<T, R>(
|
|
96
|
+
items: T[],
|
|
97
|
+
processor: (item: T) => Promise<R>,
|
|
98
|
+
options: ParallelOptions = {},
|
|
99
|
+
): Promise<ParallelResult<R>> {
|
|
100
|
+
const tasks = items.map(item => () => processor(item))
|
|
101
|
+
return parallel(tasks, options)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Map over array with parallel execution
|
|
106
|
+
*/
|
|
107
|
+
export async function parallelMap<T, R>(
|
|
108
|
+
items: T[],
|
|
109
|
+
mapper: (item: T, index: number) => Promise<R>,
|
|
110
|
+
concurrency: number = 5,
|
|
111
|
+
): Promise<R[]> {
|
|
112
|
+
const tasks = items.map((item, index) => () => mapper(item, index))
|
|
113
|
+
const result = await parallel(tasks, { concurrency, stopOnError: false })
|
|
114
|
+
|
|
115
|
+
if (result.errors.length > 0) {
|
|
116
|
+
throw new AggregateError(result.errors, 'Parallel map failed')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result.results
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Execute tasks with retry logic
|
|
124
|
+
*/
|
|
125
|
+
export async function parallelWithRetry<T>(
|
|
126
|
+
tasks: (() => Promise<T>)[],
|
|
127
|
+
options: ParallelOptions & { retries?: number, retryDelay?: number } = {},
|
|
128
|
+
): Promise<ParallelResult<T>> {
|
|
129
|
+
const retries = options.retries || 3
|
|
130
|
+
const retryDelay = options.retryDelay || 1000
|
|
131
|
+
|
|
132
|
+
const retriableTasks = tasks.map(task => async () => {
|
|
133
|
+
let lastError: Error | undefined
|
|
134
|
+
|
|
135
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
136
|
+
try {
|
|
137
|
+
return await task()
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
141
|
+
|
|
142
|
+
// Wait before retry (except on last attempt)
|
|
143
|
+
if (attempt < retries) {
|
|
144
|
+
await sleep(retryDelay * (attempt + 1)) // Exponential backoff
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw lastError
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return parallel(retriableTasks, options)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute tasks in sequence (one after another)
|
|
157
|
+
*/
|
|
158
|
+
export async function sequence<T>(
|
|
159
|
+
tasks: (() => Promise<T>)[],
|
|
160
|
+
): Promise<T[]> {
|
|
161
|
+
const results: T[] = []
|
|
162
|
+
|
|
163
|
+
for (const task of tasks) {
|
|
164
|
+
const result = await task()
|
|
165
|
+
results.push(result)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return results
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sleep for specified milliseconds
|
|
173
|
+
*/
|
|
174
|
+
function sleep(ms: number): Promise<void> {
|
|
175
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute tasks with timeout
|
|
180
|
+
*/
|
|
181
|
+
export async function withTimeout<T>(
|
|
182
|
+
task: () => Promise<T>,
|
|
183
|
+
timeoutMs: number,
|
|
184
|
+
timeoutMessage: string = 'Operation timed out',
|
|
185
|
+
): Promise<T> {
|
|
186
|
+
return Promise.race([
|
|
187
|
+
task(),
|
|
188
|
+
new Promise<T>((_, reject) =>
|
|
189
|
+
setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs),
|
|
190
|
+
),
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rate limiter for API calls
|
|
196
|
+
*/
|
|
197
|
+
export class RateLimiter {
|
|
198
|
+
private queue: (() => void)[] = []
|
|
199
|
+
private running = 0
|
|
200
|
+
private lastExecution = 0
|
|
201
|
+
|
|
202
|
+
constructor(
|
|
203
|
+
private maxConcurrent: number = 5,
|
|
204
|
+
private minInterval: number = 100, // milliseconds between calls
|
|
205
|
+
) {}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Execute task with rate limiting
|
|
209
|
+
*/
|
|
210
|
+
async execute<T>(task: () => Promise<T>): Promise<T> {
|
|
211
|
+
// Wait for available slot
|
|
212
|
+
await this.waitForSlot()
|
|
213
|
+
|
|
214
|
+
this.running++
|
|
215
|
+
this.lastExecution = Date.now()
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await task()
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
this.running--
|
|
222
|
+
this.processQueue()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Wait for available execution slot
|
|
228
|
+
*/
|
|
229
|
+
private waitForSlot(): Promise<void> {
|
|
230
|
+
if (this.running < this.maxConcurrent) {
|
|
231
|
+
const timeSinceLastExecution = Date.now() - this.lastExecution
|
|
232
|
+
|
|
233
|
+
if (timeSinceLastExecution >= this.minInterval) {
|
|
234
|
+
return Promise.resolve()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return sleep(this.minInterval - timeSinceLastExecution)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return new Promise(resolve => this.queue.push(resolve))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Process queued tasks
|
|
245
|
+
*/
|
|
246
|
+
private processQueue(): void {
|
|
247
|
+
if (this.queue.length > 0 && this.running < this.maxConcurrent) {
|
|
248
|
+
const resolve = this.queue.shift()!
|
|
249
|
+
resolve()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get stats
|
|
255
|
+
*/
|
|
256
|
+
stats(): { running: number, queued: number } {
|
|
257
|
+
return {
|
|
258
|
+
running: this.running,
|
|
259
|
+
queued: this.queue.length,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Chunk array into smaller batches
|
|
266
|
+
*/
|
|
267
|
+
export function chunk<T>(array: T[], size: number): T[][] {
|
|
268
|
+
const chunks: T[][] = []
|
|
269
|
+
|
|
270
|
+
for (let i = 0; i < array.length; i += size) {
|
|
271
|
+
chunks.push(array.slice(i, i + size))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return chunks
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Process array in chunks with parallel execution
|
|
279
|
+
*/
|
|
280
|
+
export async function processInChunks<T, R>(
|
|
281
|
+
items: T[],
|
|
282
|
+
chunkSize: number,
|
|
283
|
+
processor: (chunk: T[]) => Promise<R[]>,
|
|
284
|
+
): Promise<R[]> {
|
|
285
|
+
const chunks = chunk(items, chunkSize)
|
|
286
|
+
const results: R[] = []
|
|
287
|
+
|
|
288
|
+
for (const currentChunk of chunks) {
|
|
289
|
+
const chunkResults = await processor(currentChunk)
|
|
290
|
+
results.push(...chunkResults)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return results
|
|
294
|
+
}
|