effect-start 0.26.0 → 0.27.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/package.json +4 -2
- package/src/Entity.ts +6 -6
- package/src/FileRouterCodegen.ts +4 -4
- package/src/FileSystem.ts +4 -8
- package/src/RouteHook.ts +1 -1
- package/src/RouteSse.ts +3 -3
- package/src/SqlIntrospect.ts +2 -2
- package/src/Start.ts +102 -2
- package/src/Values.ts +11 -0
- package/src/bun/BunRoute.ts +1 -1
- package/src/bun/BunRuntime.ts +5 -5
- package/src/hyper/HyperHtml.ts +11 -7
- package/src/hyper/jsx.d.ts +1 -1
- package/src/lint/plugin.js +174 -4
- package/src/sql/SqlClient.ts +355 -0
- package/src/sql/bun/index.ts +117 -50
- package/src/sql/index.ts +1 -1
- package/src/sql/libsql/index.ts +91 -77
- package/src/sql/libsql/libsql.d.ts +4 -1
- package/src/sql/mssql/index.ts +141 -108
- package/src/sql/mssql/mssql.d.ts +1 -0
- package/src/testing/TestLogger.ts +4 -4
- package/src/x/tailwind/compile.ts +6 -14
- package/src/console/Console.ts +0 -42
- package/src/console/ConsoleErrors.ts +0 -213
- package/src/console/ConsoleLogger.ts +0 -56
- package/src/console/ConsoleMetrics.ts +0 -72
- package/src/console/ConsoleProcess.ts +0 -59
- package/src/console/ConsoleStore.ts +0 -187
- package/src/console/ConsoleTracer.ts +0 -107
- package/src/console/Simulation.ts +0 -814
- package/src/console/console.html +0 -340
- package/src/console/index.ts +0 -3
- package/src/console/routes/errors/route.tsx +0 -97
- package/src/console/routes/fiberDetail.tsx +0 -54
- package/src/console/routes/fibers/route.tsx +0 -45
- package/src/console/routes/git/route.tsx +0 -64
- package/src/console/routes/layout.tsx +0 -4
- package/src/console/routes/logs/route.tsx +0 -77
- package/src/console/routes/metrics/route.tsx +0 -36
- package/src/console/routes/route.tsx +0 -8
- package/src/console/routes/routes/route.tsx +0 -30
- package/src/console/routes/services/route.tsx +0 -21
- package/src/console/routes/system/route.tsx +0 -43
- package/src/console/routes/traceDetail.tsx +0 -22
- package/src/console/routes/traces/route.tsx +0 -81
- package/src/console/routes/tree.ts +0 -30
- package/src/console/ui/Errors.tsx +0 -76
- package/src/console/ui/Fibers.tsx +0 -321
- package/src/console/ui/Git.tsx +0 -182
- package/src/console/ui/Logs.tsx +0 -46
- package/src/console/ui/Metrics.tsx +0 -78
- package/src/console/ui/Routes.tsx +0 -125
- package/src/console/ui/Services.tsx +0 -273
- package/src/console/ui/Shell.tsx +0 -62
- package/src/console/ui/System.tsx +0 -131
- package/src/console/ui/Traces.tsx +0 -426
- package/src/sql/Sql.ts +0 -51
|
@@ -1,814 +0,0 @@
|
|
|
1
|
-
import * as Data from "effect/Data"
|
|
2
|
-
import * as Duration from "effect/Duration"
|
|
3
|
-
import * as Effect from "effect/Effect"
|
|
4
|
-
import * as Layer from "effect/Layer"
|
|
5
|
-
import * as Metric from "effect/Metric"
|
|
6
|
-
import * as MetricBoundaries from "effect/MetricBoundaries"
|
|
7
|
-
import * as Random from "effect/Random"
|
|
8
|
-
import * as Schedule from "effect/Schedule"
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
const pick = <T>(arr: ReadonlyArray<T>): Effect.Effect<T> =>
|
|
15
|
-
Random.nextIntBetween(0, arr.length).pipe(Effect.map((i) => arr[i]))
|
|
16
|
-
|
|
17
|
-
const randomMs = (min: number, max: number) =>
|
|
18
|
-
Random.nextIntBetween(min, max).pipe(Effect.map((ms) => Duration.millis(ms)))
|
|
19
|
-
|
|
20
|
-
const maybe = (pct: number, op: Effect.Effect<void, any>) =>
|
|
21
|
-
Effect.gen(function* () {
|
|
22
|
-
if ((yield* Random.nextIntBetween(0, 100)) < pct) yield* op
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Metrics
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
const httpRequestsTotal = Metric.counter("http.requests.total")
|
|
30
|
-
const httpRequestDuration = Metric.histogram(
|
|
31
|
-
"http.request.duration_ms",
|
|
32
|
-
MetricBoundaries.linear({ start: 0, width: 50, count: 20 }),
|
|
33
|
-
)
|
|
34
|
-
const activeConnections = Metric.gauge("http.active_connections")
|
|
35
|
-
const dbQueryDuration = Metric.histogram(
|
|
36
|
-
"db.query.duration_ms",
|
|
37
|
-
MetricBoundaries.linear({ start: 0, width: 10, count: 25 }),
|
|
38
|
-
)
|
|
39
|
-
const dbPoolSize = Metric.gauge("db.pool.active")
|
|
40
|
-
const cacheHits = Metric.counter("cache.hits")
|
|
41
|
-
const cacheMisses = Metric.counter("cache.misses")
|
|
42
|
-
const queueDepth = Metric.gauge("queue.depth")
|
|
43
|
-
const eventCount = Metric.counter("events.processed")
|
|
44
|
-
const retryCount = Metric.counter("retry.total")
|
|
45
|
-
const circuitBreakerTrips = Metric.counter("circuit_breaker.trips")
|
|
46
|
-
const rateLimitRejections = Metric.counter("rate_limit.rejections")
|
|
47
|
-
const serializationDuration = Metric.histogram(
|
|
48
|
-
"serialization.duration_ms",
|
|
49
|
-
MetricBoundaries.linear({ start: 0, width: 5, count: 15 }),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Simulated operations
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
const routes = [
|
|
57
|
-
{ method: "GET", path: "/api/users", handler: "UserController.list" },
|
|
58
|
-
{ method: "GET", path: "/api/users/:id", handler: "UserController.get" },
|
|
59
|
-
{ method: "POST", path: "/api/users", handler: "UserController.create" },
|
|
60
|
-
{ method: "PUT", path: "/api/users/:id", handler: "UserController.update" },
|
|
61
|
-
{ method: "DELETE", path: "/api/users/:id", handler: "UserController.delete" },
|
|
62
|
-
{ method: "GET", path: "/api/products", handler: "ProductController.list" },
|
|
63
|
-
{ method: "GET", path: "/api/products/:id", handler: "ProductController.get" },
|
|
64
|
-
{ method: "POST", path: "/api/orders", handler: "OrderController.create" },
|
|
65
|
-
{ method: "GET", path: "/api/orders/:id", handler: "OrderController.get" },
|
|
66
|
-
{ method: "PATCH", path: "/api/orders/:id/status", handler: "OrderController.updateStatus" },
|
|
67
|
-
{ method: "POST", path: "/api/auth/login", handler: "AuthController.login" },
|
|
68
|
-
{ method: "POST", path: "/api/auth/refresh", handler: "AuthController.refresh" },
|
|
69
|
-
{ method: "POST", path: "/api/auth/logout", handler: "AuthController.logout" },
|
|
70
|
-
{ method: "GET", path: "/api/search", handler: "SearchController.query" },
|
|
71
|
-
{ method: "GET", path: "/api/analytics/dashboard", handler: "AnalyticsController.dashboard" },
|
|
72
|
-
{ method: "POST", path: "/api/notifications/send", handler: "NotificationController.send" },
|
|
73
|
-
{ method: "POST", path: "/api/uploads/image", handler: "UploadController.image" },
|
|
74
|
-
{ method: "GET", path: "/api/health", handler: "HealthController.check" },
|
|
75
|
-
{ method: "GET", path: "/api/config", handler: "ConfigController.get" },
|
|
76
|
-
{ method: "POST", path: "/api/webhooks/stripe", handler: "WebhookController.stripe" },
|
|
77
|
-
] as const
|
|
78
|
-
|
|
79
|
-
const dbQueries = [
|
|
80
|
-
"SELECT * FROM users WHERE id = $1",
|
|
81
|
-
"SELECT * FROM users ORDER BY created_at DESC LIMIT 20",
|
|
82
|
-
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
|
|
83
|
-
"UPDATE users SET name = $1 WHERE id = $2",
|
|
84
|
-
"SELECT p.*, c.name AS category FROM products p JOIN categories c ON p.category_id = c.id",
|
|
85
|
-
"SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC",
|
|
86
|
-
"INSERT INTO orders (user_id, total, status) VALUES ($1, $2, $3)",
|
|
87
|
-
"SELECT u.*, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.id GROUP BY u.id",
|
|
88
|
-
"DELETE FROM sessions WHERE expires_at < NOW()",
|
|
89
|
-
"SELECT * FROM products WHERE tsv @@ plainto_tsquery($1) LIMIT 50",
|
|
90
|
-
"SELECT o.*, json_agg(oi.*) AS items FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.id = $1 GROUP BY o.id",
|
|
91
|
-
"WITH ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales DESC) rn FROM products) SELECT * FROM ranked WHERE rn <= 5",
|
|
92
|
-
"UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2 AND quantity >= $1 RETURNING quantity",
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
const cacheKeys = [
|
|
96
|
-
"user:profile:42",
|
|
97
|
-
"user:profile:108",
|
|
98
|
-
"product:listing:page:1",
|
|
99
|
-
"product:detail:77",
|
|
100
|
-
"session:abc123",
|
|
101
|
-
"rate_limit:192.168.1.1",
|
|
102
|
-
"search:results:shoes",
|
|
103
|
-
"config:feature_flags",
|
|
104
|
-
"analytics:dashboard:daily",
|
|
105
|
-
"inventory:stock:sku_4421",
|
|
106
|
-
"cart:user:42",
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
const errorMessages = [
|
|
110
|
-
"connection refused: upstream timeout after 30s",
|
|
111
|
-
"UNIQUE constraint failed: users.email",
|
|
112
|
-
"rate limit exceeded for client 10.0.3.44",
|
|
113
|
-
"invalid JWT: token expired at 2025-12-01T00:00:00Z",
|
|
114
|
-
"payment gateway returned 502",
|
|
115
|
-
"deadlock detected on table orders",
|
|
116
|
-
"request body exceeds 10MB limit",
|
|
117
|
-
"foreign key constraint: order references missing user",
|
|
118
|
-
"TLS handshake timeout with payment-service:443",
|
|
119
|
-
"DNS resolution failed for analytics.internal.svc",
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// Errors
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
class DatabaseError extends Data.TaggedError("DatabaseError")<{
|
|
127
|
-
readonly query: string
|
|
128
|
-
readonly reason: string
|
|
129
|
-
}> {}
|
|
130
|
-
|
|
131
|
-
class AuthenticationError extends Data.TaggedError("AuthenticationError")<{
|
|
132
|
-
readonly reason: string
|
|
133
|
-
readonly userId?: string
|
|
134
|
-
}> {}
|
|
135
|
-
|
|
136
|
-
class ExternalServiceError extends Data.TaggedError("ExternalServiceError")<{
|
|
137
|
-
readonly service: string
|
|
138
|
-
readonly statusCode: number
|
|
139
|
-
readonly reason: string
|
|
140
|
-
}> {}
|
|
141
|
-
|
|
142
|
-
class TimeoutError extends Data.TaggedError("TimeoutError")<{
|
|
143
|
-
readonly operation: string
|
|
144
|
-
readonly durationMs: number
|
|
145
|
-
}> {}
|
|
146
|
-
|
|
147
|
-
class ValidationError extends Data.TaggedError("ValidationError")<{
|
|
148
|
-
readonly field: string
|
|
149
|
-
readonly message: string
|
|
150
|
-
}> {}
|
|
151
|
-
|
|
152
|
-
class TaskError extends Data.TaggedError("TaskError")<{
|
|
153
|
-
readonly task: string
|
|
154
|
-
readonly reason: string
|
|
155
|
-
}> {}
|
|
156
|
-
|
|
157
|
-
class HttpError extends Data.TaggedError("HttpError")<{
|
|
158
|
-
readonly method: string
|
|
159
|
-
readonly path: string
|
|
160
|
-
readonly statusCode: number
|
|
161
|
-
readonly message: string
|
|
162
|
-
}> {}
|
|
163
|
-
|
|
164
|
-
class RateLimitError extends Data.TaggedError("RateLimitError")<{
|
|
165
|
-
readonly clientIp: string
|
|
166
|
-
readonly limit: number
|
|
167
|
-
}> {}
|
|
168
|
-
|
|
169
|
-
class CircuitBreakerError extends Data.TaggedError("CircuitBreakerError")<{
|
|
170
|
-
readonly service: string
|
|
171
|
-
readonly failureCount: number
|
|
172
|
-
}> {}
|
|
173
|
-
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// Leaf-level spans
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
const simulateDnsResolve = Effect.gen(function* () {
|
|
179
|
-
const hosts = [
|
|
180
|
-
"db-primary.internal",
|
|
181
|
-
"cache-01.internal",
|
|
182
|
-
"payment-service.prod",
|
|
183
|
-
"queue.internal",
|
|
184
|
-
"analytics.internal.svc",
|
|
185
|
-
]
|
|
186
|
-
const host = yield* pick(hosts)
|
|
187
|
-
yield* Effect.annotateCurrentSpan("dns.host", host)
|
|
188
|
-
yield* Effect.sleep(yield* randomMs(0, 5))
|
|
189
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 2) {
|
|
190
|
-
return yield* Effect.fail(
|
|
191
|
-
new TimeoutError({ operation: `dns.resolve(${host})`, durationMs: 5000 }),
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
}).pipe(Effect.withSpan("dns.resolve"))
|
|
195
|
-
|
|
196
|
-
const simulateTlsHandshake = Effect.gen(function* () {
|
|
197
|
-
yield* Effect.sleep(yield* randomMs(2, 20))
|
|
198
|
-
yield* Effect.annotateCurrentSpan("tls.version", "1.3")
|
|
199
|
-
yield* Effect.annotateCurrentSpan("tls.cipher", "TLS_AES_256_GCM_SHA384")
|
|
200
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 1) {
|
|
201
|
-
return yield* Effect.fail(new TimeoutError({ operation: "tls.handshake", durationMs: 10000 }))
|
|
202
|
-
}
|
|
203
|
-
}).pipe(Effect.withSpan("tls.handshake"))
|
|
204
|
-
|
|
205
|
-
const simulateConnectionPoolAcquire = Effect.gen(function* () {
|
|
206
|
-
const pool = yield* Random.nextIntBetween(1, 20)
|
|
207
|
-
const maxPool = 20
|
|
208
|
-
yield* Effect.annotateCurrentSpan("pool.active", pool)
|
|
209
|
-
yield* Effect.annotateCurrentSpan("pool.max", maxPool)
|
|
210
|
-
yield* dbPoolSize.pipe(Metric.set(pool))
|
|
211
|
-
yield* Effect.sleep(yield* randomMs(0, pool > 15 ? 50 : 5))
|
|
212
|
-
if (pool >= 19 && (yield* Random.nextIntBetween(0, 100)) < 30) {
|
|
213
|
-
return yield* Effect.die(new DatabaseError({ query: "", reason: "connection pool exhausted" }))
|
|
214
|
-
}
|
|
215
|
-
}).pipe(Effect.withSpan("db.pool.acquire"))
|
|
216
|
-
|
|
217
|
-
const simulateConnectionPoolRelease = Effect.gen(function* () {
|
|
218
|
-
yield* Effect.sleep(yield* randomMs(0, 1))
|
|
219
|
-
}).pipe(Effect.withSpan("db.pool.release"))
|
|
220
|
-
|
|
221
|
-
const simulateQueryParse = Effect.gen(function* () {
|
|
222
|
-
yield* Effect.sleep(yield* randomMs(0, 3))
|
|
223
|
-
yield* Effect.annotateCurrentSpan("db.parse.cached", (yield* Random.nextIntBetween(0, 100)) < 80)
|
|
224
|
-
}).pipe(Effect.withSpan("db.query.parse"))
|
|
225
|
-
|
|
226
|
-
const simulateQueryExecute = Effect.gen(function* () {
|
|
227
|
-
const query = yield* pick(dbQueries)
|
|
228
|
-
const delay = yield* randomMs(1, 80)
|
|
229
|
-
yield* Effect.annotateCurrentSpan("db.statement", query)
|
|
230
|
-
yield* Metric.update(dbQueryDuration, Math.round(Duration.toMillis(delay)))
|
|
231
|
-
yield* Effect.sleep(delay)
|
|
232
|
-
const roll = yield* Random.nextIntBetween(0, 100)
|
|
233
|
-
if (roll < 2) {
|
|
234
|
-
return yield* Effect.fail(
|
|
235
|
-
new DatabaseError({ query, reason: "deadlock detected on table orders" }),
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
if (roll < 4) {
|
|
239
|
-
return yield* Effect.fail(
|
|
240
|
-
new TimeoutError({
|
|
241
|
-
operation: "db.query.execute",
|
|
242
|
-
durationMs: Math.round(Duration.toMillis(delay)),
|
|
243
|
-
}),
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
}).pipe(Effect.withSpan("db.query.execute"))
|
|
247
|
-
|
|
248
|
-
const simulateResultDeserialization = Effect.gen(function* () {
|
|
249
|
-
const rowCount = yield* Random.nextIntBetween(0, 500)
|
|
250
|
-
yield* Effect.annotateCurrentSpan("db.rows", rowCount)
|
|
251
|
-
yield* Effect.sleep(yield* randomMs(0, rowCount > 100 ? 15 : 3))
|
|
252
|
-
}).pipe(Effect.withSpan("db.result.deserialize"))
|
|
253
|
-
|
|
254
|
-
const simulateDbQuery = Effect.gen(function* () {
|
|
255
|
-
yield* simulateConnectionPoolAcquire
|
|
256
|
-
yield* simulateQueryParse
|
|
257
|
-
yield* simulateQueryExecute
|
|
258
|
-
yield* simulateResultDeserialization
|
|
259
|
-
yield* simulateConnectionPoolRelease
|
|
260
|
-
}).pipe(Effect.withSpan("db.query"))
|
|
261
|
-
|
|
262
|
-
const simulateCacheSerialize = Effect.gen(function* () {
|
|
263
|
-
const ms = yield* Random.nextIntBetween(0, 5)
|
|
264
|
-
yield* Metric.update(serializationDuration, ms)
|
|
265
|
-
yield* Effect.sleep(Duration.millis(ms))
|
|
266
|
-
}).pipe(Effect.withSpan("cache.serialize"))
|
|
267
|
-
|
|
268
|
-
const simulateCacheDeserialize = Effect.gen(function* () {
|
|
269
|
-
const ms = yield* Random.nextIntBetween(0, 4)
|
|
270
|
-
yield* Metric.update(serializationDuration, ms)
|
|
271
|
-
yield* Effect.sleep(Duration.millis(ms))
|
|
272
|
-
}).pipe(Effect.withSpan("cache.deserialize"))
|
|
273
|
-
|
|
274
|
-
const simulateCache = Effect.gen(function* () {
|
|
275
|
-
const key = yield* pick(cacheKeys)
|
|
276
|
-
const hit = (yield* Random.nextIntBetween(0, 100)) < 75
|
|
277
|
-
yield* Effect.annotateCurrentSpan("cache.key", key)
|
|
278
|
-
yield* Effect.annotateCurrentSpan("cache.hit", hit)
|
|
279
|
-
yield* Effect.sleep(yield* randomMs(0, 3))
|
|
280
|
-
if (hit) {
|
|
281
|
-
yield* Metric.increment(cacheHits)
|
|
282
|
-
yield* simulateCacheDeserialize
|
|
283
|
-
yield* Effect.logDebug(`cache hit: ${key}`)
|
|
284
|
-
} else {
|
|
285
|
-
yield* Metric.increment(cacheMisses)
|
|
286
|
-
yield* Effect.logDebug(`cache miss: ${key}`)
|
|
287
|
-
}
|
|
288
|
-
}).pipe(Effect.withSpan("cache.lookup"))
|
|
289
|
-
|
|
290
|
-
const simulateTokenDecode = Effect.gen(function* () {
|
|
291
|
-
yield* Effect.sleep(yield* randomMs(0, 2))
|
|
292
|
-
yield* Effect.annotateCurrentSpan("jwt.alg", "RS256")
|
|
293
|
-
}).pipe(Effect.withSpan("jwt.decode"))
|
|
294
|
-
|
|
295
|
-
const simulateTokenVerify = Effect.gen(function* () {
|
|
296
|
-
yield* Effect.sleep(yield* randomMs(1, 8))
|
|
297
|
-
const roll = yield* Random.nextIntBetween(0, 100)
|
|
298
|
-
if (roll < 3) {
|
|
299
|
-
return yield* Effect.fail(
|
|
300
|
-
new AuthenticationError({ reason: "JWT expired at 2025-12-01T00:00:00Z" }),
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
if (roll < 5) {
|
|
304
|
-
return yield* Effect.fail(
|
|
305
|
-
new AuthenticationError({
|
|
306
|
-
reason: "invalid signature",
|
|
307
|
-
userId: "user_" + (yield* Random.nextIntBetween(100, 999)),
|
|
308
|
-
}),
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
}).pipe(Effect.withSpan("jwt.verify"))
|
|
312
|
-
|
|
313
|
-
const simulatePermissionCheck = Effect.gen(function* () {
|
|
314
|
-
const roles = ["admin", "editor", "viewer", "moderator"]
|
|
315
|
-
const role = yield* pick(roles)
|
|
316
|
-
yield* Effect.annotateCurrentSpan("auth.role", role)
|
|
317
|
-
yield* Effect.sleep(yield* randomMs(0, 3))
|
|
318
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 2) {
|
|
319
|
-
return yield* Effect.fail(
|
|
320
|
-
new AuthenticationError({ reason: `insufficient permissions for role: ${role}` }),
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
}).pipe(Effect.withSpan("auth.permission_check"))
|
|
324
|
-
|
|
325
|
-
const simulateAuth = Effect.gen(function* () {
|
|
326
|
-
yield* simulateTokenDecode
|
|
327
|
-
yield* simulateTokenVerify
|
|
328
|
-
yield* simulatePermissionCheck
|
|
329
|
-
yield* Effect.logDebug("token validated")
|
|
330
|
-
}).pipe(Effect.withSpan("auth.validate"))
|
|
331
|
-
|
|
332
|
-
const simulateRateLimit = Effect.gen(function* () {
|
|
333
|
-
const ips = ["10.0.3.44", "192.168.1.1", "172.16.0.55", "10.0.7.12", "203.0.113.42"]
|
|
334
|
-
const ip = yield* pick(ips)
|
|
335
|
-
const remaining = yield* Random.nextIntBetween(0, 100)
|
|
336
|
-
yield* Effect.annotateCurrentSpan("rate_limit.client_ip", ip)
|
|
337
|
-
yield* Effect.annotateCurrentSpan("rate_limit.remaining", remaining)
|
|
338
|
-
yield* Effect.sleep(yield* randomMs(0, 2))
|
|
339
|
-
if (remaining < 3) {
|
|
340
|
-
yield* Metric.increment(rateLimitRejections)
|
|
341
|
-
yield* Effect.logWarning(`rate limit near threshold for ${ip}`)
|
|
342
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 40) {
|
|
343
|
-
return yield* Effect.fail(new RateLimitError({ clientIp: ip, limit: 100 }))
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}).pipe(Effect.withSpan("middleware.rate_limit"))
|
|
347
|
-
|
|
348
|
-
const simulateCors = Effect.gen(function* () {
|
|
349
|
-
const origins = [
|
|
350
|
-
"https://app.example.com",
|
|
351
|
-
"https://admin.example.com",
|
|
352
|
-
"https://mobile.example.com",
|
|
353
|
-
"null",
|
|
354
|
-
]
|
|
355
|
-
const origin = yield* pick(origins)
|
|
356
|
-
yield* Effect.annotateCurrentSpan("cors.origin", origin)
|
|
357
|
-
yield* Effect.sleep(yield* randomMs(0, 1))
|
|
358
|
-
if (origin === "null") {
|
|
359
|
-
yield* Effect.logWarning("CORS: rejected null origin")
|
|
360
|
-
}
|
|
361
|
-
}).pipe(Effect.withSpan("middleware.cors"))
|
|
362
|
-
|
|
363
|
-
const simulateRequestParsing = Effect.gen(function* () {
|
|
364
|
-
const contentTypes = [
|
|
365
|
-
"application/json",
|
|
366
|
-
"multipart/form-data",
|
|
367
|
-
"application/x-www-form-urlencoded",
|
|
368
|
-
"text/plain",
|
|
369
|
-
]
|
|
370
|
-
const ct = yield* pick(contentTypes)
|
|
371
|
-
yield* Effect.annotateCurrentSpan("http.content_type", ct)
|
|
372
|
-
const bodySize = yield* Random.nextIntBetween(0, 50000)
|
|
373
|
-
yield* Effect.annotateCurrentSpan("http.body_size", bodySize)
|
|
374
|
-
yield* Effect.sleep(yield* randomMs(0, bodySize > 10000 ? 15 : 3))
|
|
375
|
-
if (bodySize > 40000) {
|
|
376
|
-
return yield* Effect.fail(
|
|
377
|
-
new ValidationError({ field: "body", message: "request body exceeds 10MB limit" }),
|
|
378
|
-
)
|
|
379
|
-
}
|
|
380
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 3) {
|
|
381
|
-
return yield* Effect.fail(
|
|
382
|
-
new ValidationError({ field: "body", message: "malformed JSON at position 42" }),
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
}).pipe(Effect.withSpan("http.parse_body"))
|
|
386
|
-
|
|
387
|
-
const simulateInputValidation = Effect.gen(function* () {
|
|
388
|
-
yield* Effect.sleep(yield* randomMs(0, 3))
|
|
389
|
-
const fields = ["email", "name", "amount", "quantity", "phone", "address.zip"]
|
|
390
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 5) {
|
|
391
|
-
const field = yield* pick(fields)
|
|
392
|
-
return yield* Effect.fail(
|
|
393
|
-
new ValidationError({ field, message: `invalid format for ${field}` }),
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
}).pipe(Effect.withSpan("validation.input"))
|
|
397
|
-
|
|
398
|
-
const simulateResponseSerialization = Effect.gen(function* () {
|
|
399
|
-
const formats = ["json", "msgpack", "protobuf"]
|
|
400
|
-
const format = yield* pick(formats)
|
|
401
|
-
yield* Effect.annotateCurrentSpan("serialization.format", format)
|
|
402
|
-
const ms = yield* Random.nextIntBetween(0, 10)
|
|
403
|
-
yield* Metric.update(serializationDuration, ms)
|
|
404
|
-
yield* Effect.sleep(Duration.millis(ms))
|
|
405
|
-
}).pipe(Effect.withSpan("http.serialize_response"))
|
|
406
|
-
|
|
407
|
-
const simulateCompression = Effect.gen(function* () {
|
|
408
|
-
const algos = ["gzip", "br", "none"]
|
|
409
|
-
const algo = yield* pick(algos)
|
|
410
|
-
yield* Effect.annotateCurrentSpan("compression.algorithm", algo)
|
|
411
|
-
yield* Effect.sleep(yield* randomMs(0, algo === "none" ? 1 : 8))
|
|
412
|
-
}).pipe(Effect.withSpan("http.compress"))
|
|
413
|
-
|
|
414
|
-
const simulateAccessLog = Effect.gen(function* () {
|
|
415
|
-
yield* Effect.sleep(yield* randomMs(0, 1))
|
|
416
|
-
}).pipe(Effect.withSpan("middleware.access_log"))
|
|
417
|
-
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
// Composite sub-operations with deep nesting
|
|
420
|
-
// ---------------------------------------------------------------------------
|
|
421
|
-
|
|
422
|
-
const simulateCircuitBreaker = (inner: Effect.Effect<void, any>, service: string) =>
|
|
423
|
-
Effect.gen(function* () {
|
|
424
|
-
const failures = yield* Random.nextIntBetween(0, 10)
|
|
425
|
-
yield* Effect.annotateCurrentSpan("circuit_breaker.service", service)
|
|
426
|
-
yield* Effect.annotateCurrentSpan("circuit_breaker.failure_count", failures)
|
|
427
|
-
if (failures >= 8) {
|
|
428
|
-
yield* Metric.increment(circuitBreakerTrips)
|
|
429
|
-
yield* Effect.logWarning(`circuit breaker OPEN for ${service}`)
|
|
430
|
-
return yield* Effect.fail(new CircuitBreakerError({ service, failureCount: failures }))
|
|
431
|
-
}
|
|
432
|
-
yield* inner
|
|
433
|
-
}).pipe(Effect.withSpan("circuit_breaker"))
|
|
434
|
-
|
|
435
|
-
const simulateRetry = (inner: Effect.Effect<void, any>, opName: string) =>
|
|
436
|
-
Effect.gen(function* () {
|
|
437
|
-
const maxRetries = 3
|
|
438
|
-
let attempt = 0
|
|
439
|
-
let succeeded = false
|
|
440
|
-
while (attempt < maxRetries && !succeeded) {
|
|
441
|
-
attempt++
|
|
442
|
-
yield* Effect.annotateCurrentSpan("retry.attempt", attempt)
|
|
443
|
-
const result = yield* inner.pipe(
|
|
444
|
-
Effect.map(() => true),
|
|
445
|
-
Effect.catchAll((e) => {
|
|
446
|
-
if (attempt < maxRetries) {
|
|
447
|
-
return Effect.gen(function* () {
|
|
448
|
-
yield* Metric.increment(retryCount)
|
|
449
|
-
yield* Effect.logWarning(`${opName} attempt ${attempt} failed, retrying`)
|
|
450
|
-
yield* Effect.sleep(yield* randomMs(10, 50 * attempt))
|
|
451
|
-
return false
|
|
452
|
-
})
|
|
453
|
-
}
|
|
454
|
-
return Effect.fail(e)
|
|
455
|
-
}),
|
|
456
|
-
)
|
|
457
|
-
succeeded = result
|
|
458
|
-
}
|
|
459
|
-
}).pipe(Effect.withSpan("retry"))
|
|
460
|
-
|
|
461
|
-
const simulateExternalCall = Effect.gen(function* () {
|
|
462
|
-
const services = [
|
|
463
|
-
"payment-service",
|
|
464
|
-
"email-service",
|
|
465
|
-
"notification-service",
|
|
466
|
-
"inventory-service",
|
|
467
|
-
"shipping-service",
|
|
468
|
-
"tax-service",
|
|
469
|
-
]
|
|
470
|
-
const service = yield* pick(services)
|
|
471
|
-
yield* Effect.annotateCurrentSpan("peer.service", service)
|
|
472
|
-
|
|
473
|
-
yield* simulateDnsResolve
|
|
474
|
-
yield* simulateTlsHandshake
|
|
475
|
-
|
|
476
|
-
const delay = yield* randomMs(10, 300)
|
|
477
|
-
yield* Effect.annotateCurrentSpan(
|
|
478
|
-
"http.outgoing.duration_ms",
|
|
479
|
-
Math.round(Duration.toMillis(delay)),
|
|
480
|
-
)
|
|
481
|
-
yield* Effect.sleep(delay)
|
|
482
|
-
|
|
483
|
-
const roll = yield* Random.nextIntBetween(0, 100)
|
|
484
|
-
if (roll < 4) {
|
|
485
|
-
yield* Effect.logError(`${service} responded with 503`)
|
|
486
|
-
return yield* Effect.fail(
|
|
487
|
-
new ExternalServiceError({ service, statusCode: 503, reason: "service unavailable" }),
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
if (roll < 6) {
|
|
491
|
-
yield* Effect.logError(`${service} timed out after ${Math.round(Duration.toMillis(delay))}ms`)
|
|
492
|
-
return yield* Effect.fail(
|
|
493
|
-
new TimeoutError({
|
|
494
|
-
operation: `${service} call`,
|
|
495
|
-
durationMs: Math.round(Duration.toMillis(delay)),
|
|
496
|
-
}),
|
|
497
|
-
)
|
|
498
|
-
}
|
|
499
|
-
if (roll < 8) {
|
|
500
|
-
yield* Effect.logError(`${service} responded with 502`)
|
|
501
|
-
return yield* Effect.fail(
|
|
502
|
-
new ExternalServiceError({ service, statusCode: 502, reason: "bad gateway" }),
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
|
-
}).pipe(Effect.withSpan("http.outgoing"))
|
|
506
|
-
|
|
507
|
-
const simulateExternalCallWithResilience = Effect.gen(function* () {
|
|
508
|
-
const service = yield* pick([
|
|
509
|
-
"payment-service",
|
|
510
|
-
"email-service",
|
|
511
|
-
"notification-service",
|
|
512
|
-
"inventory-service",
|
|
513
|
-
])
|
|
514
|
-
yield* simulateCircuitBreaker(simulateRetry(simulateExternalCall, `external.${service}`), service)
|
|
515
|
-
}).pipe(Effect.withSpan("external.resilient_call"))
|
|
516
|
-
|
|
517
|
-
const simulateSearchQuery = Effect.gen(function* () {
|
|
518
|
-
const terms = ["shoes", "laptop", "organic food", "headphones", "gift ideas"]
|
|
519
|
-
const term = yield* pick(terms)
|
|
520
|
-
yield* Effect.annotateCurrentSpan("search.query", term)
|
|
521
|
-
|
|
522
|
-
yield* simulateCache
|
|
523
|
-
|
|
524
|
-
yield* Effect.gen(function* () {
|
|
525
|
-
yield* Effect.sleep(yield* randomMs(5, 40))
|
|
526
|
-
yield* Effect.annotateCurrentSpan("search.engine", "elasticsearch")
|
|
527
|
-
yield* Effect.annotateCurrentSpan("search.index", "products_v3")
|
|
528
|
-
const hits = yield* Random.nextIntBetween(0, 200)
|
|
529
|
-
yield* Effect.annotateCurrentSpan("search.hits", hits)
|
|
530
|
-
}).pipe(Effect.withSpan("search.execute"))
|
|
531
|
-
|
|
532
|
-
yield* Effect.gen(function* () {
|
|
533
|
-
yield* Effect.sleep(yield* randomMs(1, 10))
|
|
534
|
-
yield* Effect.annotateCurrentSpan("search.boost", "relevance+recency")
|
|
535
|
-
}).pipe(Effect.withSpan("search.rank"))
|
|
536
|
-
|
|
537
|
-
yield* Effect.gen(function* () {
|
|
538
|
-
yield* Effect.sleep(yield* randomMs(0, 5))
|
|
539
|
-
}).pipe(Effect.withSpan("search.highlight"))
|
|
540
|
-
}).pipe(Effect.withSpan("search.pipeline"))
|
|
541
|
-
|
|
542
|
-
const simulateFileUpload = Effect.gen(function* () {
|
|
543
|
-
const fileSize = yield* Random.nextIntBetween(1000, 5_000_000)
|
|
544
|
-
yield* Effect.annotateCurrentSpan("upload.size_bytes", fileSize)
|
|
545
|
-
|
|
546
|
-
yield* Effect.gen(function* () {
|
|
547
|
-
yield* Effect.sleep(yield* randomMs(1, 10))
|
|
548
|
-
const types = ["image/jpeg", "image/png", "application/pdf", "image/webp"]
|
|
549
|
-
const mime = yield* pick(types)
|
|
550
|
-
yield* Effect.annotateCurrentSpan("upload.mime", mime)
|
|
551
|
-
if ((yield* Random.nextIntBetween(0, 100)) < 3) {
|
|
552
|
-
return yield* Effect.fail(
|
|
553
|
-
new ValidationError({ field: "file", message: "unsupported mime type" }),
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
}).pipe(Effect.withSpan("upload.validate_mime"))
|
|
557
|
-
|
|
558
|
-
yield* Effect.gen(function* () {
|
|
559
|
-
yield* Effect.sleep(yield* randomMs(0, 5))
|
|
560
|
-
}).pipe(Effect.withSpan("upload.virus_scan"))
|
|
561
|
-
|
|
562
|
-
yield* Effect.gen(function* () {
|
|
563
|
-
yield* Effect.sleep(yield* randomMs(5, 50))
|
|
564
|
-
yield* Effect.annotateCurrentSpan("upload.bucket", "user-uploads-prod")
|
|
565
|
-
}).pipe(Effect.withSpan("upload.store_s3"))
|
|
566
|
-
|
|
567
|
-
yield* Effect.gen(function* () {
|
|
568
|
-
yield* simulateDbQuery
|
|
569
|
-
}).pipe(Effect.withSpan("upload.persist_metadata"))
|
|
570
|
-
}).pipe(Effect.withSpan("upload.pipeline"))
|
|
571
|
-
|
|
572
|
-
const simulateOrderWorkflow = Effect.gen(function* () {
|
|
573
|
-
yield* Effect.annotateCurrentSpan("order.workflow", "create")
|
|
574
|
-
|
|
575
|
-
yield* simulateInputValidation
|
|
576
|
-
|
|
577
|
-
yield* Effect.gen(function* () {
|
|
578
|
-
yield* simulateDbQuery
|
|
579
|
-
yield* Effect.sleep(yield* randomMs(1, 10))
|
|
580
|
-
}).pipe(Effect.withSpan("order.check_inventory"))
|
|
581
|
-
|
|
582
|
-
yield* Effect.gen(function* () {
|
|
583
|
-
yield* simulateExternalCallWithResilience
|
|
584
|
-
}).pipe(Effect.withSpan("order.process_payment"))
|
|
585
|
-
|
|
586
|
-
yield* Effect.gen(function* () {
|
|
587
|
-
yield* simulateDbQuery
|
|
588
|
-
}).pipe(Effect.withSpan("order.persist"))
|
|
589
|
-
|
|
590
|
-
yield* Effect.gen(function* () {
|
|
591
|
-
yield* simulateCache
|
|
592
|
-
yield* simulateCacheSerialize
|
|
593
|
-
}).pipe(Effect.withSpan("order.invalidate_cache"))
|
|
594
|
-
|
|
595
|
-
yield* Effect.gen(function* () {
|
|
596
|
-
yield* Effect.sleep(yield* randomMs(1, 10))
|
|
597
|
-
yield* Effect.logInfo("order confirmation email queued")
|
|
598
|
-
}).pipe(Effect.withSpan("order.queue_notification"))
|
|
599
|
-
}).pipe(Effect.withSpan("order.workflow"))
|
|
600
|
-
|
|
601
|
-
const simulateAnalyticsPipeline = Effect.gen(function* () {
|
|
602
|
-
yield* Effect.gen(function* () {
|
|
603
|
-
yield* simulateDbQuery
|
|
604
|
-
yield* simulateDbQuery
|
|
605
|
-
}).pipe(Effect.withSpan("analytics.fetch_raw"))
|
|
606
|
-
|
|
607
|
-
yield* Effect.gen(function* () {
|
|
608
|
-
yield* Effect.sleep(yield* randomMs(5, 30))
|
|
609
|
-
yield* Effect.annotateCurrentSpan("analytics.aggregation", "time_series")
|
|
610
|
-
}).pipe(Effect.withSpan("analytics.aggregate"))
|
|
611
|
-
|
|
612
|
-
yield* Effect.gen(function* () {
|
|
613
|
-
yield* simulateCache
|
|
614
|
-
}).pipe(Effect.withSpan("analytics.cache_result"))
|
|
615
|
-
|
|
616
|
-
yield* Effect.gen(function* () {
|
|
617
|
-
yield* Effect.sleep(yield* randomMs(2, 15))
|
|
618
|
-
yield* Effect.annotateCurrentSpan("analytics.format", "dashboard_v2")
|
|
619
|
-
}).pipe(Effect.withSpan("analytics.transform"))
|
|
620
|
-
}).pipe(Effect.withSpan("analytics.pipeline"))
|
|
621
|
-
|
|
622
|
-
// ---------------------------------------------------------------------------
|
|
623
|
-
// Background tasks (also deeper now)
|
|
624
|
-
// ---------------------------------------------------------------------------
|
|
625
|
-
|
|
626
|
-
const simulateBackgroundTask = Effect.gen(function* () {
|
|
627
|
-
const tasks = [
|
|
628
|
-
"process-webhook",
|
|
629
|
-
"send-email",
|
|
630
|
-
"generate-report",
|
|
631
|
-
"sync-inventory",
|
|
632
|
-
"cleanup-sessions",
|
|
633
|
-
"rebuild-search-index",
|
|
634
|
-
"process-refund",
|
|
635
|
-
"generate-invoice-pdf",
|
|
636
|
-
"sync-crm",
|
|
637
|
-
]
|
|
638
|
-
const task = yield* pick(tasks)
|
|
639
|
-
yield* Effect.logInfo(`starting background task: ${task}`)
|
|
640
|
-
|
|
641
|
-
yield* Effect.gen(function* () {
|
|
642
|
-
yield* Effect.sleep(yield* randomMs(0, 5))
|
|
643
|
-
yield* Effect.annotateCurrentSpan(
|
|
644
|
-
"task.priority",
|
|
645
|
-
yield* pick(["low", "normal", "high", "critical"]),
|
|
646
|
-
)
|
|
647
|
-
}).pipe(Effect.withSpan("task.dequeue"))
|
|
648
|
-
|
|
649
|
-
yield* Effect.gen(function* () {
|
|
650
|
-
yield* simulateDbQuery
|
|
651
|
-
}).pipe(Effect.withSpan("task.load_context"))
|
|
652
|
-
|
|
653
|
-
yield* Effect.gen(function* () {
|
|
654
|
-
yield* Effect.sleep(yield* randomMs(20, 300))
|
|
655
|
-
const roll = yield* Random.nextIntBetween(0, 100)
|
|
656
|
-
if (roll < 4) {
|
|
657
|
-
return yield* Effect.fail(new TaskError({ task, reason: "execution exceeded 30s deadline" }))
|
|
658
|
-
}
|
|
659
|
-
if (roll < 7) {
|
|
660
|
-
const err = yield* pick(errorMessages)
|
|
661
|
-
yield* Effect.logError(`task ${task} failed: ${err}`)
|
|
662
|
-
return yield* Effect.fail(new TaskError({ task, reason: err }))
|
|
663
|
-
}
|
|
664
|
-
if (roll < 9) {
|
|
665
|
-
return yield* Effect.die(new TaskError({ task, reason: "out of memory" }))
|
|
666
|
-
}
|
|
667
|
-
}).pipe(Effect.withSpan("task.execute"))
|
|
668
|
-
|
|
669
|
-
yield* maybe(
|
|
670
|
-
60,
|
|
671
|
-
Effect.gen(function* () {
|
|
672
|
-
yield* simulateExternalCall
|
|
673
|
-
}).pipe(Effect.withSpan("task.external_dependency")),
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
yield* Effect.gen(function* () {
|
|
677
|
-
yield* simulateDbQuery
|
|
678
|
-
}).pipe(Effect.withSpan("task.persist_result"))
|
|
679
|
-
|
|
680
|
-
yield* Effect.gen(function* () {
|
|
681
|
-
yield* Effect.sleep(yield* randomMs(0, 3))
|
|
682
|
-
}).pipe(Effect.withSpan("task.ack"))
|
|
683
|
-
|
|
684
|
-
yield* Metric.increment(eventCount)
|
|
685
|
-
yield* Effect.logInfo(`completed background task: ${task}`)
|
|
686
|
-
}).pipe(Effect.withSpan("task.background"))
|
|
687
|
-
|
|
688
|
-
// ---------------------------------------------------------------------------
|
|
689
|
-
// Simulate a single HTTP request (deep middleware + handler pipeline)
|
|
690
|
-
// ---------------------------------------------------------------------------
|
|
691
|
-
|
|
692
|
-
const simulateRequest = Effect.gen(function* () {
|
|
693
|
-
const route = yield* pick(routes)
|
|
694
|
-
const statusCodes = [200, 200, 200, 200, 200, 201, 204, 301, 400, 401, 404, 500]
|
|
695
|
-
|
|
696
|
-
yield* Metric.increment(httpRequestsTotal)
|
|
697
|
-
yield* Metric.increment(activeConnections)
|
|
698
|
-
|
|
699
|
-
const result = yield* Effect.gen(function* () {
|
|
700
|
-
yield* Effect.annotateCurrentSpan("http.method", route.method)
|
|
701
|
-
yield* Effect.annotateCurrentSpan("http.route", route.path)
|
|
702
|
-
yield* Effect.annotateCurrentSpan("handler", route.handler)
|
|
703
|
-
|
|
704
|
-
// middleware chain
|
|
705
|
-
yield* simulateAccessLog
|
|
706
|
-
yield* simulateCors
|
|
707
|
-
yield* simulateRateLimit
|
|
708
|
-
yield* simulateRequestParsing
|
|
709
|
-
|
|
710
|
-
// auth (most routes)
|
|
711
|
-
yield* maybe(70, simulateAuth)
|
|
712
|
-
|
|
713
|
-
// handler body — pick a complex workflow or simple CRUD based on the route
|
|
714
|
-
const handler = route.handler
|
|
715
|
-
if (handler === "OrderController.create" || handler === "OrderController.updateStatus") {
|
|
716
|
-
yield* simulateOrderWorkflow
|
|
717
|
-
} else if (handler === "SearchController.query") {
|
|
718
|
-
yield* simulateSearchQuery
|
|
719
|
-
} else if (handler === "UploadController.image") {
|
|
720
|
-
yield* simulateFileUpload
|
|
721
|
-
} else if (handler === "AnalyticsController.dashboard") {
|
|
722
|
-
yield* simulateAnalyticsPipeline
|
|
723
|
-
} else if (handler === "WebhookController.stripe") {
|
|
724
|
-
yield* simulateInputValidation
|
|
725
|
-
yield* simulateExternalCallWithResilience
|
|
726
|
-
yield* simulateDbQuery
|
|
727
|
-
} else {
|
|
728
|
-
// standard CRUD
|
|
729
|
-
yield* maybe(50, simulateCache)
|
|
730
|
-
yield* simulateDbQuery
|
|
731
|
-
yield* maybe(25, simulateExternalCall)
|
|
732
|
-
yield* maybe(30, simulateDbQuery) // secondary query
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// response pipeline
|
|
736
|
-
yield* simulateResponseSerialization
|
|
737
|
-
yield* maybe(40, simulateCompression)
|
|
738
|
-
|
|
739
|
-
const status = yield* pick(statusCodes)
|
|
740
|
-
yield* Effect.annotateCurrentSpan("http.status_code", status)
|
|
741
|
-
|
|
742
|
-
if (status >= 500) {
|
|
743
|
-
const err = yield* pick(errorMessages)
|
|
744
|
-
yield* Effect.logError(`${route.method} ${route.path} → ${status}: ${err}`)
|
|
745
|
-
return yield* Effect.fail(
|
|
746
|
-
new HttpError({
|
|
747
|
-
method: route.method,
|
|
748
|
-
path: route.path,
|
|
749
|
-
statusCode: status,
|
|
750
|
-
message: err,
|
|
751
|
-
}),
|
|
752
|
-
)
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (status >= 400) {
|
|
756
|
-
yield* Effect.logWarning(`${route.method} ${route.path} → ${status}`)
|
|
757
|
-
} else {
|
|
758
|
-
yield* Effect.logInfo(`${route.method} ${route.path} → ${status}`)
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return status
|
|
762
|
-
}).pipe(Effect.withSpan(`${route.method} ${route.path}`))
|
|
763
|
-
|
|
764
|
-
yield* activeConnections.pipe(Metric.set(0))
|
|
765
|
-
|
|
766
|
-
const durationMs = yield* Random.nextIntBetween(5, 500)
|
|
767
|
-
yield* Metric.update(httpRequestDuration, durationMs)
|
|
768
|
-
|
|
769
|
-
return result
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
// ---------------------------------------------------------------------------
|
|
773
|
-
// Simulation loops
|
|
774
|
-
// ---------------------------------------------------------------------------
|
|
775
|
-
|
|
776
|
-
const requestLoop = Effect.gen(function* () {
|
|
777
|
-
yield* Effect.logInfo("simulation: request loop started")
|
|
778
|
-
yield* Effect.schedule(
|
|
779
|
-
Effect.gen(function* () {
|
|
780
|
-
const burst = yield* Random.nextIntBetween(1, 4)
|
|
781
|
-
yield* Effect.forEach(
|
|
782
|
-
Array.from({ length: burst }, (_, i) => i),
|
|
783
|
-
() => simulateRequest.pipe(Effect.fork),
|
|
784
|
-
{ concurrency: "unbounded" },
|
|
785
|
-
)
|
|
786
|
-
}),
|
|
787
|
-
Schedule.jittered(Schedule.spaced("500 millis")),
|
|
788
|
-
)
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
const backgroundLoop = Effect.gen(function* () {
|
|
792
|
-
yield* Effect.logInfo("simulation: background task loop started")
|
|
793
|
-
yield* Effect.schedule(
|
|
794
|
-
Effect.gen(function* () {
|
|
795
|
-
yield* Effect.fork(simulateBackgroundTask)
|
|
796
|
-
const depth = yield* Random.nextIntBetween(0, 25)
|
|
797
|
-
yield* queueDepth.pipe(Metric.set(depth))
|
|
798
|
-
}),
|
|
799
|
-
Schedule.jittered(Schedule.spaced("2 seconds")),
|
|
800
|
-
)
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
// ---------------------------------------------------------------------------
|
|
804
|
-
// Public API
|
|
805
|
-
// ---------------------------------------------------------------------------
|
|
806
|
-
|
|
807
|
-
export const layer = Layer.scopedDiscard(
|
|
808
|
-
Effect.gen(function* () {
|
|
809
|
-
yield* Effect.logInfo("simulation layer starting")
|
|
810
|
-
yield* Effect.forkScoped(requestLoop)
|
|
811
|
-
yield* Effect.forkScoped(backgroundLoop)
|
|
812
|
-
yield* Effect.logInfo("simulation layer ready")
|
|
813
|
-
}),
|
|
814
|
-
)
|