agentic-team-templates 0.13.1 → 0.14.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 +6 -1
- package/package.json +1 -1
- package/src/index.js +22 -2
- package/src/index.test.js +5 -0
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Kotlin Expert Development Guide
|
|
2
|
+
|
|
3
|
+
Principal-level guidelines for Kotlin engineering. Deep language mastery, coroutines, multiplatform, and idiomatic patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This guide applies to:
|
|
10
|
+
- Backend services (Ktor, Spring Boot, Quarkus)
|
|
11
|
+
- Android applications (Jetpack Compose, Architecture Components)
|
|
12
|
+
- Kotlin Multiplatform (KMP) — shared code across platforms
|
|
13
|
+
- CLI tools and scripting
|
|
14
|
+
- Libraries and Maven/Gradle artifacts
|
|
15
|
+
- Data processing and streaming
|
|
16
|
+
|
|
17
|
+
### Core Philosophy
|
|
18
|
+
|
|
19
|
+
Kotlin is a pragmatic language. It gives you safety, expressiveness, and interoperability — use all three.
|
|
20
|
+
|
|
21
|
+
- **Null safety is the foundation.** The type system distinguishes nullable from non-nullable. No `!!` without proof.
|
|
22
|
+
- **Immutability by default.** `val` over `var`, immutable collections, data classes.
|
|
23
|
+
- **Conciseness without cleverness.** Less code doesn't mean unreadable code.
|
|
24
|
+
- **Coroutines are structured.** Every coroutine has a scope, every scope has a lifecycle.
|
|
25
|
+
- **Interop is a feature.** Use Java libraries freely, but write Kotlin idiomatically.
|
|
26
|
+
- **If you don't know, say so.** Admitting uncertainty is professional.
|
|
27
|
+
|
|
28
|
+
### Key Principles
|
|
29
|
+
|
|
30
|
+
1. **Null Safety Is Non-Negotiable** — No `!!` without justification. Safe calls, elvis, smart casts
|
|
31
|
+
2. **Immutability by Default** — `val`, `List` (not `MutableList`), data classes, `copy()`
|
|
32
|
+
3. **Structured Concurrency** — Every coroutine in a scope. No `GlobalScope`
|
|
33
|
+
4. **Extension Functions Over Utility Classes** — Extend types at the call site
|
|
34
|
+
5. **Sealed Types for State Modeling** — Exhaustive `when`, impossible states are compile errors
|
|
35
|
+
|
|
36
|
+
### Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
project/
|
|
40
|
+
├── src/main/kotlin/com/example/myapp/
|
|
41
|
+
│ ├── Application.kt
|
|
42
|
+
│ ├── config/
|
|
43
|
+
│ ├── domain/
|
|
44
|
+
│ │ ├── model/
|
|
45
|
+
│ │ ├── service/
|
|
46
|
+
│ │ └── event/
|
|
47
|
+
│ ├── application/
|
|
48
|
+
│ ├── infrastructure/
|
|
49
|
+
│ └── api/
|
|
50
|
+
├── src/test/kotlin/
|
|
51
|
+
├── build.gradle.kts
|
|
52
|
+
└── Dockerfile
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Language Features
|
|
58
|
+
|
|
59
|
+
### Null Safety
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
val displayName = user?.name ?: "Anonymous"
|
|
63
|
+
val userId = request.userId ?: throw IllegalArgumentException("userId required")
|
|
64
|
+
// Never: user!!.name — use requireNotNull() with a message
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Data Classes and Value Classes
|
|
68
|
+
|
|
69
|
+
```kotlin
|
|
70
|
+
data class Order(val id: OrderId, val items: List<OrderItem>, val status: OrderStatus)
|
|
71
|
+
val updated = order.copy(status = OrderStatus.SHIPPED)
|
|
72
|
+
|
|
73
|
+
@JvmInline
|
|
74
|
+
value class UserId(val value: String) // Zero-overhead type safety
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Sealed Types
|
|
78
|
+
|
|
79
|
+
```kotlin
|
|
80
|
+
sealed interface Result<out T> {
|
|
81
|
+
data class Success<T>(val value: T) : Result<T>
|
|
82
|
+
data class Failure(val error: AppError) : Result<Nothing>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun describe(result: Result<User>): String = when (result) {
|
|
86
|
+
is Result.Success -> "User: ${result.value.name}"
|
|
87
|
+
is Result.Failure -> "Error: ${result.error}"
|
|
88
|
+
// Exhaustive — compiler enforces all cases
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Extension Functions and Scope Functions
|
|
93
|
+
|
|
94
|
+
```kotlin
|
|
95
|
+
fun String.toSlug(): String = lowercase().replace(Regex("[^a-z0-9\\s-]"), "").replace(Regex("\\s+"), "-")
|
|
96
|
+
|
|
97
|
+
val connection = HttpClient().apply { timeout = 30.seconds; retries = 3 }
|
|
98
|
+
val user = createUser(request).also { logger.info("Created: ${it.id}") }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Coroutines
|
|
104
|
+
|
|
105
|
+
### Structured Concurrency
|
|
106
|
+
|
|
107
|
+
```kotlin
|
|
108
|
+
suspend fun loadDashboard(userId: UserId): Dashboard = coroutineScope {
|
|
109
|
+
val user = async { userService.findById(userId) }
|
|
110
|
+
val orders = async { orderService.findRecent(userId) }
|
|
111
|
+
Dashboard(user = user.await(), orders = orders.await())
|
|
112
|
+
}
|
|
113
|
+
// If any async fails, ALL are cancelled
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Flow
|
|
117
|
+
|
|
118
|
+
```kotlin
|
|
119
|
+
val state: StateFlow<UiState> = _state.asStateFlow()
|
|
120
|
+
|
|
121
|
+
fun observeUsers(): Flow<List<User>> = flow {
|
|
122
|
+
while (true) {
|
|
123
|
+
emit(userRepository.findAll())
|
|
124
|
+
delay(5.seconds)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Rules
|
|
130
|
+
|
|
131
|
+
- No `GlobalScope` — use lifecycle-bound scopes
|
|
132
|
+
- No `runBlocking` in production (except `main()` and tests)
|
|
133
|
+
- No `Thread.sleep()` — use `delay()`
|
|
134
|
+
- Always rethrow `CancellationException`
|
|
135
|
+
- Use `supervisorScope` when child failures should be independent
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
### Sealed Result Types
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
when (val result = userService.register(request)) {
|
|
145
|
+
is Result.Success -> call.respond(HttpStatusCode.Created, result.value)
|
|
146
|
+
is Result.Failure -> when (result.error) {
|
|
147
|
+
is AppError.Validation -> call.respond(HttpStatusCode.BadRequest, result.error)
|
|
148
|
+
is AppError.Conflict -> call.respond(HttpStatusCode.Conflict, result.error)
|
|
149
|
+
is AppError.NotFound -> call.respond(HttpStatusCode.NotFound, result.error)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Validation
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
require(customerId.isNotBlank()) { "customerId must not be blank" }
|
|
158
|
+
check(order.status == OrderStatus.PAID) { "Can only ship paid orders" }
|
|
159
|
+
val user = requireNotNull(userRepo.findById(id)) { "User $id must exist" }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Frameworks
|
|
165
|
+
|
|
166
|
+
### Ktor
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
fun Route.userRoutes() {
|
|
170
|
+
route("/users") {
|
|
171
|
+
get("/{id}") {
|
|
172
|
+
val id = UserId(call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest))
|
|
173
|
+
// ...
|
|
174
|
+
}
|
|
175
|
+
post { val request = call.receive<CreateUserRequest>(); /* ... */ }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Spring Boot
|
|
181
|
+
|
|
182
|
+
```kotlin
|
|
183
|
+
@RestController
|
|
184
|
+
@RequestMapping("/api/v1/orders")
|
|
185
|
+
class OrderController(private val orderService: OrderService) {
|
|
186
|
+
@GetMapping("/{id}")
|
|
187
|
+
suspend fun getById(@PathVariable id: UUID): ResponseEntity<OrderResponse> { /* ... */ }
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Exposed (SQL)
|
|
192
|
+
|
|
193
|
+
```kotlin
|
|
194
|
+
object Users : Table("users") {
|
|
195
|
+
val id = uuid("id").autoGenerate()
|
|
196
|
+
val name = varchar("name", 200)
|
|
197
|
+
val email = varchar("email", 255).uniqueIndex()
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Testing
|
|
204
|
+
|
|
205
|
+
### Framework Stack
|
|
206
|
+
|
|
207
|
+
| Tool | Purpose |
|
|
208
|
+
|------|---------|
|
|
209
|
+
| JUnit 5 / Kotest | Test framework |
|
|
210
|
+
| MockK | Kotlin-native mocking |
|
|
211
|
+
| kotlinx-coroutines-test | Coroutine testing |
|
|
212
|
+
| Testcontainers | Real databases/services |
|
|
213
|
+
| Ktor Test | HTTP endpoint testing |
|
|
214
|
+
|
|
215
|
+
### Test Structure
|
|
216
|
+
|
|
217
|
+
```kotlin
|
|
218
|
+
@Test
|
|
219
|
+
fun `create with valid items returns success`() = runTest {
|
|
220
|
+
coEvery { inventoryClient.checkAvailability("SKU-001", 2) } returns true
|
|
221
|
+
val result = sut.create(request)
|
|
222
|
+
assertThat(result).isInstanceOf(Result.Success::class.java)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Performance
|
|
229
|
+
|
|
230
|
+
### Key Patterns
|
|
231
|
+
|
|
232
|
+
- `inline` functions for higher-order function hot paths
|
|
233
|
+
- `value class` for zero-overhead type wrappers
|
|
234
|
+
- Sequences for large collection chains (lazy evaluation)
|
|
235
|
+
- Pre-size collections with `HashMap(expectedSize)`
|
|
236
|
+
- `buildList`/`buildString` for single-allocation construction
|
|
237
|
+
- Buffered channels for coroutine throughput
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Tooling
|
|
242
|
+
|
|
243
|
+
### Essential Stack
|
|
244
|
+
|
|
245
|
+
| Tool | Purpose |
|
|
246
|
+
|------|---------|
|
|
247
|
+
| Gradle (Kotlin DSL) | Build system |
|
|
248
|
+
| Detekt | Static analysis |
|
|
249
|
+
| ktlint | Code formatting |
|
|
250
|
+
| kotlinx.serialization | Compile-time serialization |
|
|
251
|
+
| kotlin-logging | Structured logging |
|
|
252
|
+
| Koin | Dependency injection |
|
|
253
|
+
|
|
254
|
+
### CI Essentials
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
./gradlew check # Build + test + analysis
|
|
258
|
+
./gradlew detekt # Static analysis
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Definition of Done
|
|
264
|
+
|
|
265
|
+
A Kotlin feature is complete when:
|
|
266
|
+
|
|
267
|
+
- [ ] Compiles with zero warnings (`allWarningsAsErrors = true`)
|
|
268
|
+
- [ ] All tests pass
|
|
269
|
+
- [ ] No `!!` without documented justification
|
|
270
|
+
- [ ] No `var` where `val` suffices
|
|
271
|
+
- [ ] No `MutableList`/`MutableMap` exposed in public APIs
|
|
272
|
+
- [ ] Coroutines use structured concurrency (no `GlobalScope`)
|
|
273
|
+
- [ ] Nullable types handled explicitly
|
|
274
|
+
- [ ] Detekt reports zero findings
|
|
275
|
+
- [ ] No `TODO` without an associated issue
|
|
276
|
+
- [ ] Code reviewed and approved
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Swift Concurrency
|
|
2
|
+
|
|
3
|
+
Structured concurrency with async/await, actors, and task groups. No more callback pyramids.
|
|
4
|
+
|
|
5
|
+
## async/await
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Async functions
|
|
9
|
+
func fetchUser(id: UUID) async throws -> User {
|
|
10
|
+
let (data, response) = try await urlSession.data(from: userURL(id))
|
|
11
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
12
|
+
httpResponse.statusCode == 200 else {
|
|
13
|
+
throw APIError.invalidResponse
|
|
14
|
+
}
|
|
15
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Calling async code
|
|
19
|
+
let user = try await fetchUser(id: userId)
|
|
20
|
+
|
|
21
|
+
// Async let — parallel execution
|
|
22
|
+
async let user = fetchUser(id: userId)
|
|
23
|
+
async let orders = fetchOrders(userId: userId)
|
|
24
|
+
async let preferences = fetchPreferences(userId: userId)
|
|
25
|
+
|
|
26
|
+
let dashboard = try await Dashboard(
|
|
27
|
+
user: user,
|
|
28
|
+
orders: orders,
|
|
29
|
+
preferences: preferences
|
|
30
|
+
)
|
|
31
|
+
// All three requests run concurrently
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Task and TaskGroup
|
|
35
|
+
|
|
36
|
+
```swift
|
|
37
|
+
// Unstructured task (use sparingly)
|
|
38
|
+
Task {
|
|
39
|
+
do {
|
|
40
|
+
let result = try await processData()
|
|
41
|
+
await MainActor.run { updateUI(with: result) }
|
|
42
|
+
} catch {
|
|
43
|
+
await MainActor.run { showError(error) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Task group — dynamic parallelism
|
|
48
|
+
func fetchAllUsers(ids: [UUID]) async throws -> [User] {
|
|
49
|
+
try await withThrowingTaskGroup(of: User.self) { group in
|
|
50
|
+
for id in ids {
|
|
51
|
+
group.addTask {
|
|
52
|
+
try await fetchUser(id: id)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var users: [User] = []
|
|
57
|
+
for try await user in group {
|
|
58
|
+
users.append(user)
|
|
59
|
+
}
|
|
60
|
+
return users
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Task cancellation
|
|
65
|
+
func longRunningProcess() async throws {
|
|
66
|
+
for item in items {
|
|
67
|
+
try Task.checkCancellation() // Throws if cancelled
|
|
68
|
+
await process(item)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Cooperative cancellation
|
|
73
|
+
Task {
|
|
74
|
+
let result = try await longRunningProcess()
|
|
75
|
+
}
|
|
76
|
+
// Later:
|
|
77
|
+
task.cancel() // Cooperative — task must check
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Actors
|
|
81
|
+
|
|
82
|
+
```swift
|
|
83
|
+
// Actor — thread-safe mutable state
|
|
84
|
+
actor ImageCache {
|
|
85
|
+
private var cache: [URL: UIImage] = [:]
|
|
86
|
+
|
|
87
|
+
func image(for url: URL) -> UIImage? {
|
|
88
|
+
cache[url]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func store(_ image: UIImage, for url: URL) {
|
|
92
|
+
cache[url] = image
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func clear() {
|
|
96
|
+
cache.removeAll()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Using actors
|
|
101
|
+
let cache = ImageCache()
|
|
102
|
+
let image = await cache.image(for: url) // await required for actor isolation
|
|
103
|
+
|
|
104
|
+
// nonisolated — opt out for non-mutable access
|
|
105
|
+
actor UserStore {
|
|
106
|
+
let id: UUID // Immutable, safe without isolation
|
|
107
|
+
|
|
108
|
+
nonisolated var description: String {
|
|
109
|
+
"UserStore(\(id))"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## MainActor
|
|
115
|
+
|
|
116
|
+
```swift
|
|
117
|
+
// MainActor — UI thread safety
|
|
118
|
+
@MainActor
|
|
119
|
+
class UserViewModel: ObservableObject {
|
|
120
|
+
@Published var users: [User] = []
|
|
121
|
+
@Published var isLoading = false
|
|
122
|
+
@Published var error: AppError?
|
|
123
|
+
|
|
124
|
+
func loadUsers() async {
|
|
125
|
+
isLoading = true
|
|
126
|
+
defer { isLoading = false }
|
|
127
|
+
|
|
128
|
+
do {
|
|
129
|
+
users = try await userService.fetchAll()
|
|
130
|
+
} catch {
|
|
131
|
+
self.error = AppError(error)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MainActor.run for one-off UI updates
|
|
137
|
+
func processInBackground() async {
|
|
138
|
+
let result = await heavyComputation()
|
|
139
|
+
await MainActor.run {
|
|
140
|
+
self.displayResult = result
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Sendable
|
|
146
|
+
|
|
147
|
+
```swift
|
|
148
|
+
// Sendable — safe to pass across concurrency domains
|
|
149
|
+
struct UserDTO: Sendable {
|
|
150
|
+
let id: UUID
|
|
151
|
+
let name: String
|
|
152
|
+
let email: String
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// @Sendable closures
|
|
156
|
+
func performAsync(_ work: @Sendable @escaping () async -> Void) {
|
|
157
|
+
Task { await work() }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// @unchecked Sendable — manual safety guarantee
|
|
161
|
+
final class ThreadSafeCache: @unchecked Sendable {
|
|
162
|
+
private let lock = NSLock()
|
|
163
|
+
private var storage: [String: Any] = [:]
|
|
164
|
+
|
|
165
|
+
func get(_ key: String) -> Any? {
|
|
166
|
+
lock.lock()
|
|
167
|
+
defer { lock.unlock() }
|
|
168
|
+
return storage[key]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## AsyncSequence and AsyncStream
|
|
174
|
+
|
|
175
|
+
```swift
|
|
176
|
+
// AsyncStream for bridging callback APIs
|
|
177
|
+
func notifications(named name: Notification.Name) -> AsyncStream<Notification> {
|
|
178
|
+
AsyncStream { continuation in
|
|
179
|
+
let observer = NotificationCenter.default.addObserver(
|
|
180
|
+
forName: name, object: nil, queue: nil
|
|
181
|
+
) { notification in
|
|
182
|
+
continuation.yield(notification)
|
|
183
|
+
}
|
|
184
|
+
continuation.onTermination = { _ in
|
|
185
|
+
NotificationCenter.default.removeObserver(observer)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Consuming async sequences
|
|
191
|
+
for await notification in notifications(named: .userDidLogin) {
|
|
192
|
+
handleLogin(notification)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// AsyncSequence transformations
|
|
196
|
+
let validUsers = userStream
|
|
197
|
+
.filter { $0.isActive }
|
|
198
|
+
.map { UserViewModel(user: $0) }
|
|
199
|
+
.prefix(10)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Rules
|
|
203
|
+
|
|
204
|
+
- No `DispatchQueue.main.async` in new code — use `@MainActor` or `MainActor.run`
|
|
205
|
+
- No completion handlers in new code — use `async`/`await`
|
|
206
|
+
- No `DispatchGroup` — use `TaskGroup`
|
|
207
|
+
- Always check `Task.isCancelled` or call `Task.checkCancellation()` in loops
|
|
208
|
+
- Mark types as `Sendable` when passed across concurrency domains
|
|
209
|
+
- Use actors for shared mutable state, not locks (in new code)
|
|
210
|
+
- `Task {}` is unstructured — prefer structured concurrency (`async let`, `TaskGroup`)
|
|
211
|
+
|
|
212
|
+
## Anti-Patterns
|
|
213
|
+
|
|
214
|
+
```swift
|
|
215
|
+
// Never: fire-and-forget tasks without error handling
|
|
216
|
+
Task { try await riskyOperation() } // Errors silently swallowed
|
|
217
|
+
// Use: Task with do/catch
|
|
218
|
+
|
|
219
|
+
// Never: blocking the main thread
|
|
220
|
+
DispatchQueue.main.sync { /* ... */ } // Deadlock risk
|
|
221
|
+
// Use: @MainActor or MainActor.run
|
|
222
|
+
|
|
223
|
+
// Never: capturing self strongly in long-lived tasks
|
|
224
|
+
Task { [self] in await self.process() } // Potential retain cycle
|
|
225
|
+
// Use: Task { [weak self] in await self?.process() }
|
|
226
|
+
|
|
227
|
+
// Never: using GCD for new concurrent code
|
|
228
|
+
DispatchQueue.global().async { /* ... */ }
|
|
229
|
+
// Use: Task { /* ... */ } with async/await
|
|
230
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Swift Error Handling
|
|
2
|
+
|
|
3
|
+
Typed errors, Result, and defensive validation. Make failure states explicit.
|
|
4
|
+
|
|
5
|
+
## Error Types
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Domain-specific errors with associated values
|
|
9
|
+
enum APIError: Error, LocalizedError {
|
|
10
|
+
case networkUnavailable
|
|
11
|
+
case unauthorized
|
|
12
|
+
case notFound(resource: String, id: String)
|
|
13
|
+
case serverError(statusCode: Int, message: String)
|
|
14
|
+
case decodingFailed(type: String, underlying: Error)
|
|
15
|
+
case rateLimited(retryAfter: TimeInterval)
|
|
16
|
+
|
|
17
|
+
var errorDescription: String? {
|
|
18
|
+
switch self {
|
|
19
|
+
case .networkUnavailable:
|
|
20
|
+
"Network connection is unavailable"
|
|
21
|
+
case .unauthorized:
|
|
22
|
+
"Authentication required"
|
|
23
|
+
case .notFound(let resource, let id):
|
|
24
|
+
"\(resource) with ID '\(id)' not found"
|
|
25
|
+
case .serverError(let code, let message):
|
|
26
|
+
"Server error \(code): \(message)"
|
|
27
|
+
case .decodingFailed(let type, let error):
|
|
28
|
+
"Failed to decode \(type): \(error.localizedDescription)"
|
|
29
|
+
case .rateLimited(let retryAfter):
|
|
30
|
+
"Rate limited. Retry after \(retryAfter)s"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## do/catch
|
|
37
|
+
|
|
38
|
+
```swift
|
|
39
|
+
// Specific error handling
|
|
40
|
+
func loadUser(id: UUID) async {
|
|
41
|
+
do {
|
|
42
|
+
let user = try await userService.fetch(id: id)
|
|
43
|
+
self.user = user
|
|
44
|
+
} catch let error as APIError {
|
|
45
|
+
switch error {
|
|
46
|
+
case .notFound:
|
|
47
|
+
self.state = .notFound
|
|
48
|
+
case .unauthorized:
|
|
49
|
+
self.state = .needsLogin
|
|
50
|
+
case .networkUnavailable:
|
|
51
|
+
self.state = .offline
|
|
52
|
+
default:
|
|
53
|
+
self.state = .error(error)
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Catch-all for unexpected errors
|
|
57
|
+
self.state = .error(AppError.unexpected(error))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// try? — convert to optional (use when failure is acceptable)
|
|
62
|
+
let cachedImage = try? imageCache.load(key: url.absoluteString)
|
|
63
|
+
|
|
64
|
+
// try! — only when failure is a programmer error
|
|
65
|
+
let regex = try! NSRegularExpression(pattern: "^[a-z]+$")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Result Type
|
|
69
|
+
|
|
70
|
+
```swift
|
|
71
|
+
// Result for synchronous operations
|
|
72
|
+
func validate(email: String) -> Result<Email, ValidationError> {
|
|
73
|
+
guard !email.isEmpty else {
|
|
74
|
+
return .failure(.empty("email"))
|
|
75
|
+
}
|
|
76
|
+
guard email.contains("@") else {
|
|
77
|
+
return .failure(.invalidFormat("email"))
|
|
78
|
+
}
|
|
79
|
+
return .success(Email(rawValue: email))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pattern matching on Result
|
|
83
|
+
switch validate(email: input) {
|
|
84
|
+
case .success(let email):
|
|
85
|
+
createAccount(email: email)
|
|
86
|
+
case .failure(let error):
|
|
87
|
+
showValidationError(error)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Result with map/flatMap
|
|
91
|
+
let userName = fetchUser(id: userId)
|
|
92
|
+
.map { $0.name }
|
|
93
|
+
.mapError { AppError.network($0) }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Guard and Preconditions
|
|
97
|
+
|
|
98
|
+
```swift
|
|
99
|
+
// Guard — early exit for invalid state
|
|
100
|
+
func processOrder(_ order: Order) throws {
|
|
101
|
+
guard order.items.isEmpty == false else {
|
|
102
|
+
throw OrderError.emptyOrder
|
|
103
|
+
}
|
|
104
|
+
guard order.status == .confirmed else {
|
|
105
|
+
throw OrderError.invalidStatus(order.status)
|
|
106
|
+
}
|
|
107
|
+
guard let paymentMethod = order.paymentMethod else {
|
|
108
|
+
throw OrderError.noPaymentMethod
|
|
109
|
+
}
|
|
110
|
+
// Happy path continues here, all values available
|
|
111
|
+
charge(paymentMethod, for: order)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// precondition — programmer errors (removed in -Ounchecked)
|
|
115
|
+
func element(at index: Int) -> Element {
|
|
116
|
+
precondition(index >= 0 && index < count, "Index \(index) out of bounds")
|
|
117
|
+
return storage[index]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// assert — debug-only checks
|
|
121
|
+
func configure(retryCount: Int) {
|
|
122
|
+
assert(retryCount > 0, "retryCount must be positive")
|
|
123
|
+
self.maxRetries = retryCount
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// fatalError — truly impossible states
|
|
127
|
+
func handle(_ state: AppState) {
|
|
128
|
+
switch state {
|
|
129
|
+
case .ready: start()
|
|
130
|
+
case .running: continue_()
|
|
131
|
+
// If new states are added, this will force handling them
|
|
132
|
+
@unknown default:
|
|
133
|
+
fatalError("Unhandled state: \(state)")
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Typed Throws (Swift 6+)
|
|
139
|
+
|
|
140
|
+
```swift
|
|
141
|
+
// Typed throws — callers know exact error type
|
|
142
|
+
func parse(json: Data) throws(ParseError) -> Config {
|
|
143
|
+
guard let dict = try? JSONSerialization.jsonObject(with: json) as? [String: Any] else {
|
|
144
|
+
throw .invalidJSON
|
|
145
|
+
}
|
|
146
|
+
guard let name = dict["name"] as? String else {
|
|
147
|
+
throw .missingField("name")
|
|
148
|
+
}
|
|
149
|
+
return Config(name: name)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Caller gets typed error without casting
|
|
153
|
+
do {
|
|
154
|
+
let config = try parse(json: data)
|
|
155
|
+
} catch {
|
|
156
|
+
// error is ParseError, not any Error
|
|
157
|
+
switch error {
|
|
158
|
+
case .invalidJSON: handleInvalidJSON()
|
|
159
|
+
case .missingField(let name): handleMissingField(name)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Error Recovery Patterns
|
|
165
|
+
|
|
166
|
+
```swift
|
|
167
|
+
// Retry with exponential backoff
|
|
168
|
+
func fetchWithRetry<T>(
|
|
169
|
+
maxAttempts: Int = 3,
|
|
170
|
+
operation: () async throws -> T
|
|
171
|
+
) async throws -> T {
|
|
172
|
+
var lastError: Error?
|
|
173
|
+
for attempt in 0..<maxAttempts {
|
|
174
|
+
do {
|
|
175
|
+
return try await operation()
|
|
176
|
+
} catch {
|
|
177
|
+
lastError = error
|
|
178
|
+
if attempt < maxAttempts - 1 {
|
|
179
|
+
let delay = pow(2.0, Double(attempt))
|
|
180
|
+
try await Task.sleep(for: .seconds(delay))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw lastError!
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fallback chain
|
|
188
|
+
func loadAvatar(for user: User) async -> UIImage {
|
|
189
|
+
if let cached = avatarCache[user.id] { return cached }
|
|
190
|
+
if let downloaded = try? await downloadAvatar(user.avatarURL) { return downloaded }
|
|
191
|
+
return UIImage(systemName: "person.circle")!
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Anti-Patterns
|
|
196
|
+
|
|
197
|
+
```swift
|
|
198
|
+
// Never: catching and ignoring errors
|
|
199
|
+
do { try operation() } catch { } // Silent failure
|
|
200
|
+
// Use: at minimum, log the error
|
|
201
|
+
|
|
202
|
+
// Never: using try! on fallible operations
|
|
203
|
+
let data = try! Data(contentsOf: fileURL) // Crash on missing file
|
|
204
|
+
// Use: do/try/catch or try? with fallback
|
|
205
|
+
|
|
206
|
+
// Never: throwing generic Error
|
|
207
|
+
throw NSError(domain: "", code: 0) // No context
|
|
208
|
+
// Use: domain-specific error types
|
|
209
|
+
|
|
210
|
+
// Never: error types without context
|
|
211
|
+
enum AppError: Error { case failed } // Useless for debugging
|
|
212
|
+
// Use: associated values with context
|
|
213
|
+
```
|