@coralai/sps-cli 0.42.0 → 0.43.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 +34 -3
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Swift — Testing
|
|
2
|
+
|
|
3
|
+
XCTest and Swift Testing (Swift 6+). For TDD, see `coding-standards/references/tdd.md`.
|
|
4
|
+
|
|
5
|
+
## Runner choice
|
|
6
|
+
|
|
7
|
+
- **XCTest** — mature, ships with Xcode, required for UI tests.
|
|
8
|
+
- **Swift Testing** — Swift 6 stdlib, cleaner API, macros for parametrized tests. Use for new unit tests when your toolchain supports it.
|
|
9
|
+
|
|
10
|
+
Both can coexist in the same target.
|
|
11
|
+
|
|
12
|
+
## XCTest
|
|
13
|
+
|
|
14
|
+
```swift
|
|
15
|
+
import XCTest
|
|
16
|
+
@testable import MyApp
|
|
17
|
+
|
|
18
|
+
final class UserServiceTests: XCTestCase {
|
|
19
|
+
func test_createUser_returnsPersistedUser() async throws {
|
|
20
|
+
let service = UserService(repo: InMemoryUserRepo())
|
|
21
|
+
let user = try await service.create(email: "a@x.com")
|
|
22
|
+
XCTAssertEqual(user.email, "a@x.com")
|
|
23
|
+
XCTAssertFalse(user.id.isEmpty)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func test_createUser_rejectsEmptyEmail() async {
|
|
27
|
+
let service = UserService(repo: InMemoryUserRepo())
|
|
28
|
+
do {
|
|
29
|
+
_ = try await service.create(email: "")
|
|
30
|
+
XCTFail("expected ValidationError")
|
|
31
|
+
} catch is ValidationError {
|
|
32
|
+
// expected
|
|
33
|
+
} catch {
|
|
34
|
+
XCTFail("unexpected error: \(error)")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Name tests `test_behaviour_expectation`. Structure: **Arrange → Act → Assert**.
|
|
41
|
+
|
|
42
|
+
## Swift Testing (Swift 6+)
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
import Testing
|
|
46
|
+
@testable import MyApp
|
|
47
|
+
|
|
48
|
+
struct UserServiceTests {
|
|
49
|
+
@Test
|
|
50
|
+
func createUserReturnsPersistedUser() async throws {
|
|
51
|
+
let service = UserService(repo: InMemoryUserRepo())
|
|
52
|
+
let user = try await service.create(email: "a@x.com")
|
|
53
|
+
#expect(user.email == "a@x.com")
|
|
54
|
+
#expect(!user.id.isEmpty)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
func createUserRejectsEmptyEmail() async throws {
|
|
59
|
+
let service = UserService(repo: InMemoryUserRepo())
|
|
60
|
+
await #expect(throws: ValidationError.self) {
|
|
61
|
+
try await service.create(email: "")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Cleaner: `#expect(...)` macro, `@Test` annotation, real async support.
|
|
68
|
+
|
|
69
|
+
## Parametrized tests
|
|
70
|
+
|
|
71
|
+
```swift
|
|
72
|
+
@Test("add cases", arguments: [
|
|
73
|
+
(1, 2, 3),
|
|
74
|
+
(0, 0, 0),
|
|
75
|
+
(-1, 1, 0),
|
|
76
|
+
])
|
|
77
|
+
func add(a: Int, b: Int, expected: Int) {
|
|
78
|
+
#expect(MyApp.add(a, b) == expected)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
XCTest equivalent requires manual loops or `XCTestCase` subclasses.
|
|
83
|
+
|
|
84
|
+
## Assertions
|
|
85
|
+
|
|
86
|
+
| XCTest | Swift Testing |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `XCTAssertEqual(a, b)` | `#expect(a == b)` |
|
|
89
|
+
| `XCTAssertTrue(x)` | `#expect(x)` |
|
|
90
|
+
| `XCTAssertNil(x)` | `#expect(x == nil)` |
|
|
91
|
+
| `XCTAssertThrowsError { ... }` | `#expect(throws: ...) { ... }` |
|
|
92
|
+
| `XCTFail("msg")` | `Issue.record("msg")` |
|
|
93
|
+
|
|
94
|
+
## Fakes over mocks
|
|
95
|
+
|
|
96
|
+
Hand-roll fakes; Swift's strong type system makes them cheap.
|
|
97
|
+
|
|
98
|
+
```swift
|
|
99
|
+
final class InMemoryUserRepo: UserRepositoryProtocol {
|
|
100
|
+
private var users: [String: User] = [:]
|
|
101
|
+
func find(id: String) async -> User? { users[id] }
|
|
102
|
+
func save(_ u: User) async { users[u.id] = u }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For mock libraries, `Cuckoo` and `Mockable` exist; most teams find hand-rolled cheaper in Swift than in JVM.
|
|
107
|
+
|
|
108
|
+
## Async tests
|
|
109
|
+
|
|
110
|
+
Swift Testing supports `async` natively. XCTest also, from iOS 13+ / macOS 10.15+:
|
|
111
|
+
|
|
112
|
+
```swift
|
|
113
|
+
func test_loads() async throws {
|
|
114
|
+
let user = try await service.load(id: "u1")
|
|
115
|
+
XCTAssertEqual(user.id, "u1")
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For code using completion callbacks (legacy), use `XCTestExpectation`:
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
func test_legacy() {
|
|
123
|
+
let exp = expectation(description: "callback")
|
|
124
|
+
legacy { result in
|
|
125
|
+
XCTAssertNotNil(result)
|
|
126
|
+
exp.fulfill()
|
|
127
|
+
}
|
|
128
|
+
wait(for: [exp], timeout: 5)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
New code shouldn't need this — bridge to `async` with `withCheckedContinuation`.
|
|
133
|
+
|
|
134
|
+
## Timing / clock control
|
|
135
|
+
|
|
136
|
+
Mock time with a `Clock` protocol:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
protocol Clock { func now() -> Date }
|
|
140
|
+
|
|
141
|
+
final class FixedClock: Clock {
|
|
142
|
+
var time: Date
|
|
143
|
+
init(_ t: Date) { self.time = t }
|
|
144
|
+
func now() -> Date { time }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For `Task.sleep` in tests, use `ContinuousClock` with a manually advanced test clock (Swift Testing / swift-async-algorithms).
|
|
149
|
+
|
|
150
|
+
Swift Testing integrates with the new `Clock` primitives and supports virtual time.
|
|
151
|
+
|
|
152
|
+
## UI tests — XCUITest
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
final class AppUITests: XCTestCase {
|
|
156
|
+
func test_login_success() {
|
|
157
|
+
let app = XCUIApplication()
|
|
158
|
+
app.launch()
|
|
159
|
+
app.textFields["email"].tap()
|
|
160
|
+
app.textFields["email"].typeText("a@x.com")
|
|
161
|
+
app.buttons["Continue"].tap()
|
|
162
|
+
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Rules:
|
|
168
|
+
- Use `accessibilityIdentifier`, not labels — labels break when copy changes.
|
|
169
|
+
- Reset app state in `setUp`; UI tests are order-dependent if you don't.
|
|
170
|
+
- Keep to happy paths and critical flows; they're slow and brittle.
|
|
171
|
+
|
|
172
|
+
## Snapshot tests
|
|
173
|
+
|
|
174
|
+
`swift-snapshot-testing` (`point-free`) for views, JSON, formatted strings.
|
|
175
|
+
|
|
176
|
+
```swift
|
|
177
|
+
import SnapshotTesting
|
|
178
|
+
import XCTest
|
|
179
|
+
|
|
180
|
+
final class UserViewTests: XCTestCase {
|
|
181
|
+
func test_userView() {
|
|
182
|
+
let vc = UserViewController(user: .sample)
|
|
183
|
+
assertSnapshot(matching: vc, as: .image)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Review diffs deliberately; don't mass-accept.
|
|
189
|
+
|
|
190
|
+
## Coverage
|
|
191
|
+
|
|
192
|
+
Xcode can emit `.xcresult` with coverage; `xcrun xccov view --report` for CLI. Set a threshold in your `fastlane` / CI.
|
|
193
|
+
|
|
194
|
+
## Performance tests
|
|
195
|
+
|
|
196
|
+
```swift
|
|
197
|
+
func test_perf_parsing() {
|
|
198
|
+
measure {
|
|
199
|
+
_ = parse(sampleInput)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Xcode stores baselines; regressions highlight on re-run. Keep perf tests to critical paths; they're slow and machine-dependent.
|
|
205
|
+
|
|
206
|
+
## Test plans
|
|
207
|
+
|
|
208
|
+
Use Xcode test plans to split fast unit tests, slower integration, UI tests into separate CI stages:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
UnitTests.xctestplan # fast, ~seconds
|
|
212
|
+
IntegrationTests.xctestplan # real deps, minutes
|
|
213
|
+
UITests.xctestplan # UI, slow
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Run unit on every PR; integration and UI on merge-to-main.
|
|
217
|
+
|
|
218
|
+
## Anti-patterns
|
|
219
|
+
|
|
220
|
+
| Anti-pattern | Fix |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `Thread.sleep` / `sleep()` in tests to wait for async | `XCTestExpectation` or `async/await` |
|
|
223
|
+
| `try!` inside a test | Use `try` + `XCTAssertNoThrow` or Swift Testing `#expect(throws:)` |
|
|
224
|
+
| Shared state across tests via singletons | Reset in `setUp`; prefer DI |
|
|
225
|
+
| Snapshot test accepted without review | Always diff before committing |
|
|
226
|
+
| UI tests finding by label (`"Log in"`) | Use `accessibilityIdentifier` |
|
|
227
|
+
| Mocking the thing you're testing | Test via real public API |
|
|
228
|
+
| `XCTFail` + guard + `return` scattered inline | Use Swift Testing `#expect(throws:)` or helper methods |
|
|
229
|
+
| Tests depending on device locale / time | Inject locale / clock |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript
|
|
3
|
+
description: TypeScript language skill — types, idioms, async, tooling. Pair with end skills (`backend`, `frontend`, `mobile`) for architecture and with `coding-standards` for cross-language principles.
|
|
4
|
+
origin: ecc-fork + original (https://github.com/affaan-m/everything-claude-code, MIT)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TypeScript
|
|
8
|
+
|
|
9
|
+
Type system, idioms, async, tooling. **Language-focused**. Architecture belongs to end skills; general principles (TDD, naming, error strategy) live in `coding-standards`.
|
|
10
|
+
|
|
11
|
+
## When to load
|
|
12
|
+
|
|
13
|
+
- Project primary language is TypeScript (Node.js / Deno / Bun / browser)
|
|
14
|
+
- Reviewing TS code
|
|
15
|
+
- Designing types, generics, discriminated unions
|
|
16
|
+
- Setting up `tsconfig.json`, build, test tooling
|
|
17
|
+
|
|
18
|
+
## Core principles
|
|
19
|
+
|
|
20
|
+
1. **`strict: true`, always.** Non-negotiable. The rest of TS is barely worth using without it.
|
|
21
|
+
2. **No `any`.** Use `unknown` + narrowing if you don't know the shape. Every `any` in review gets a blocker.
|
|
22
|
+
3. **Types describe intent.** Prefer `UserId` branded type over bare `string`; use discriminated unions over boolean flags.
|
|
23
|
+
4. **Inference first.** Let TS infer; add annotations on public signatures and where inference is wrong or opaque.
|
|
24
|
+
5. **`const` by default, `let` only when reassigning.** Never `var`.
|
|
25
|
+
6. **Immutability where practical.** `readonly` on fields, `ReadonlyArray<T>` for inputs.
|
|
26
|
+
7. **`===` only.** `==` has JS quirks; there is no legitimate reason to use it in new code.
|
|
27
|
+
8. **Explicit error types at boundaries** (HTTP, queue, FFI). Inside, throw freely.
|
|
28
|
+
|
|
29
|
+
## How to use references
|
|
30
|
+
|
|
31
|
+
| Reference | When to load |
|
|
32
|
+
|---|---|
|
|
33
|
+
| [`references/types.md`](references/types.md) | Generics, unions, discriminated unions, conditional types, utility types, brands |
|
|
34
|
+
| [`references/idioms.md`](references/idioms.md) | Destructuring, optional chaining, nullish coalescing, modules, enums vs. union |
|
|
35
|
+
| [`references/async.md`](references/async.md) | Promises, async/await, error propagation, cancellation (AbortController), concurrency |
|
|
36
|
+
| [`references/errors.md`](references/errors.md) | Error classes, `Result` / `neverthrow`, `try/catch`, re-throw semantics |
|
|
37
|
+
| [`references/testing.md`](references/testing.md) | Vitest / Jest, mocking, fixtures, integration tests |
|
|
38
|
+
| [`references/tooling.md`](references/tooling.md) | `tsconfig`, ESLint, formatter, bundlers, monorepo layout |
|
|
39
|
+
|
|
40
|
+
## Forbidden patterns (auto-reject)
|
|
41
|
+
|
|
42
|
+
- `any` without a comment explaining why
|
|
43
|
+
- `as X` casts without runtime validation (use a schema validator at boundaries)
|
|
44
|
+
- `==` / `!=`
|
|
45
|
+
- `var`
|
|
46
|
+
- Unhandled promise (`.then()` without `.catch()`, or an `await` without containing `try`)
|
|
47
|
+
- `Function` / `Object` as a type
|
|
48
|
+
- Default-exporting everything in new code (named exports — one public name per import)
|
|
49
|
+
- Enums for string constants (use literal string unions instead — smaller, tree-shakeable)
|
|
50
|
+
- Mutating function parameters
|
|
51
|
+
- `console.log` left in committed code
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# TypeScript — Async
|
|
2
|
+
|
|
3
|
+
Promises, async/await, error propagation, cancellation, concurrency.
|
|
4
|
+
|
|
5
|
+
## `async/await` is sugar for promises
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// These are equivalent
|
|
9
|
+
async function f() { const x = await g(); return x + 1; }
|
|
10
|
+
function f() { return g().then(x => x + 1); }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Prefer `async/await` for linear flows, `.then` chains for pipelines on a single value.
|
|
14
|
+
|
|
15
|
+
## Every promise must be awaited or handled
|
|
16
|
+
|
|
17
|
+
Unhandled rejections crash Node (in strict mode) or log cryptic warnings.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// ❌ fire-and-forget
|
|
21
|
+
sendEmail(user.email); // if it rejects → unhandled
|
|
22
|
+
doThing().then(logSuccess); // no .catch
|
|
23
|
+
|
|
24
|
+
// ✅
|
|
25
|
+
await sendEmail(user.email);
|
|
26
|
+
|
|
27
|
+
// ✅ intentional fire-and-forget: use .catch
|
|
28
|
+
void sendEmail(user.email).catch(err => log.error(err));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
TS/ESLint rules: `@typescript-eslint/no-floating-promises` catches these.
|
|
32
|
+
|
|
33
|
+
## Error propagation
|
|
34
|
+
|
|
35
|
+
Throws inside `async` become rejections. `try/catch` catches them.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
async function load() {
|
|
39
|
+
try {
|
|
40
|
+
const r = await fetch(url);
|
|
41
|
+
if (!r.ok) throw new HttpError(r.status);
|
|
42
|
+
return await r.json();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e instanceof HttpError && e.status === 404) return null;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`catch (e: unknown)` — always. `e` is not an `Error` in JS; could be anything thrown.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
try { ... } catch (e) {
|
|
54
|
+
if (e instanceof Error) log.error(e.message);
|
|
55
|
+
else log.error('non-Error thrown', { e });
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Concurrency
|
|
60
|
+
|
|
61
|
+
### Parallel, all must succeed
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const [u, o, p] = await Promise.all([
|
|
65
|
+
getUser(id),
|
|
66
|
+
getOrders(id),
|
|
67
|
+
getPrefs(id),
|
|
68
|
+
]);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
One rejection → whole thing rejects; other in-flight promises continue but their results are discarded.
|
|
72
|
+
|
|
73
|
+
### Parallel, any may fail
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const results = await Promise.allSettled(tasks);
|
|
77
|
+
for (const r of results) {
|
|
78
|
+
if (r.status === 'fulfilled') use(r.value);
|
|
79
|
+
else log.warn(r.reason);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### First to finish wins
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const winner = await Promise.race([req, timeout(5000)]);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### First to succeed (any)
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const first = await Promise.any([primary(), secondary(), tertiary()]);
|
|
93
|
+
// rejects only if all fail
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Sequential vs. parallel — don't accidentally serialize
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// ❌ serial (each waits for the prior)
|
|
100
|
+
for (const id of ids) {
|
|
101
|
+
await fetchUser(id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ✅ parallel
|
|
105
|
+
await Promise.all(ids.map(fetchUser));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Sequential is correct when order matters or when you want backpressure. Parallel is often what you wanted but wrote wrong.
|
|
109
|
+
|
|
110
|
+
## Bounded parallelism
|
|
111
|
+
|
|
112
|
+
Unlimited parallelism blows up memory and hammers downstreams. Bound it.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import pLimit from 'p-limit';
|
|
116
|
+
|
|
117
|
+
const limit = pLimit(10);
|
|
118
|
+
const results = await Promise.all(ids.map(id => limit(() => fetchUser(id))));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or chunk manually:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
async function inChunks<T, R>(xs: T[], size: number, fn: (x: T) => Promise<R>) {
|
|
125
|
+
const out: R[] = [];
|
|
126
|
+
for (let i = 0; i < xs.length; i += size) {
|
|
127
|
+
const chunk = xs.slice(i, i + size);
|
|
128
|
+
out.push(...await Promise.all(chunk.map(fn)));
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Cancellation — `AbortController`
|
|
135
|
+
|
|
136
|
+
The standard cancellation primitive for fetch, streams, timers.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const ac = new AbortController();
|
|
140
|
+
setTimeout(() => ac.abort(), 5000);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const r = await fetch(url, { signal: ac.signal });
|
|
144
|
+
return await r.json();
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
147
|
+
// cancelled
|
|
148
|
+
} else {
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For your own async functions, accept a `signal` and check / propagate it:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
async function work({ signal }: { signal?: AbortSignal }) {
|
|
158
|
+
if (signal?.aborted) throw new DOMException('aborted', 'AbortError');
|
|
159
|
+
signal?.addEventListener('abort', () => { /* clean up */ }, { once: true });
|
|
160
|
+
...
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Timeouts — `AbortSignal.timeout` (modern)
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Falls back to `AbortController` + `setTimeout` on older runtimes.
|
|
171
|
+
|
|
172
|
+
## Retry with backoff
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
async function withRetry<T>(
|
|
176
|
+
fn: () => Promise<T>,
|
|
177
|
+
{ attempts = 3, baseMs = 100, maxMs = 10_000 } = {},
|
|
178
|
+
): Promise<T> {
|
|
179
|
+
for (let i = 0; i < attempts; i++) {
|
|
180
|
+
try { return await fn(); }
|
|
181
|
+
catch (e) {
|
|
182
|
+
if (i === attempts - 1) throw e;
|
|
183
|
+
const delay = Math.min(maxMs, baseMs * 2 ** i) * (0.5 + Math.random());
|
|
184
|
+
await new Promise(r => setTimeout(r, delay));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw new Error('unreachable');
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Only retry idempotent operations. See `backend/references/resilience.md`.
|
|
192
|
+
|
|
193
|
+
## Async iterators & streams
|
|
194
|
+
|
|
195
|
+
For data you consume one chunk at a time.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
async function* readLines(stream: ReadableStream<Uint8Array>) {
|
|
199
|
+
const decoder = new TextDecoder();
|
|
200
|
+
const reader = stream.getReader();
|
|
201
|
+
let buf = '';
|
|
202
|
+
while (true) {
|
|
203
|
+
const { value, done } = await reader.read();
|
|
204
|
+
if (done) break;
|
|
205
|
+
buf += decoder.decode(value, { stream: true });
|
|
206
|
+
const lines = buf.split('\n');
|
|
207
|
+
buf = lines.pop()!;
|
|
208
|
+
for (const line of lines) yield line;
|
|
209
|
+
}
|
|
210
|
+
if (buf) yield buf;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for await (const line of readLines(resp.body!)) {
|
|
214
|
+
process(line);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Microtasks & the event loop
|
|
219
|
+
|
|
220
|
+
`await` queues continuation as a microtask — runs before macro-tasks (`setTimeout`) even at delay 0.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
Promise.resolve().then(() => console.log('A')); // microtask
|
|
224
|
+
setTimeout(() => console.log('B'), 0); // macrotask
|
|
225
|
+
// A then B
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
You rarely need to know this, until you're debugging an out-of-order log.
|
|
229
|
+
|
|
230
|
+
## Anti-patterns
|
|
231
|
+
|
|
232
|
+
| Anti-pattern | Fix |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `new Promise((res) => { ... res(x) })` wrapping a callback API | Use `util.promisify` (Node) or a library wrapper |
|
|
235
|
+
| Rejecting with a string | Always `new Error(...)` |
|
|
236
|
+
| Nested `.then` chains | Flatten with `await` |
|
|
237
|
+
| `.catch` without re-throw on unknown errors | Narrow or rethrow |
|
|
238
|
+
| `await` inside a `.forEach` | Use `for...of` or `Promise.all` |
|
|
239
|
+
| `async` function returning `Promise<Promise<T>>` | Unnecessary; one `async` is enough |
|
|
240
|
+
| Dangling `setTimeout`/`setInterval` in React effects | Return cleanup or use a library |
|
|
241
|
+
| Fire-and-forget without `.catch` | Silent failures; add `.catch(log)` |
|