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.
Files changed (58) hide show
  1. package/package.json +4 -2
  2. package/src/Entity.ts +6 -6
  3. package/src/FileRouterCodegen.ts +4 -4
  4. package/src/FileSystem.ts +4 -8
  5. package/src/RouteHook.ts +1 -1
  6. package/src/RouteSse.ts +3 -3
  7. package/src/SqlIntrospect.ts +2 -2
  8. package/src/Start.ts +102 -2
  9. package/src/Values.ts +11 -0
  10. package/src/bun/BunRoute.ts +1 -1
  11. package/src/bun/BunRuntime.ts +5 -5
  12. package/src/hyper/HyperHtml.ts +11 -7
  13. package/src/hyper/jsx.d.ts +1 -1
  14. package/src/lint/plugin.js +174 -4
  15. package/src/sql/SqlClient.ts +355 -0
  16. package/src/sql/bun/index.ts +117 -50
  17. package/src/sql/index.ts +1 -1
  18. package/src/sql/libsql/index.ts +91 -77
  19. package/src/sql/libsql/libsql.d.ts +4 -1
  20. package/src/sql/mssql/index.ts +141 -108
  21. package/src/sql/mssql/mssql.d.ts +1 -0
  22. package/src/testing/TestLogger.ts +4 -4
  23. package/src/x/tailwind/compile.ts +6 -14
  24. package/src/console/Console.ts +0 -42
  25. package/src/console/ConsoleErrors.ts +0 -213
  26. package/src/console/ConsoleLogger.ts +0 -56
  27. package/src/console/ConsoleMetrics.ts +0 -72
  28. package/src/console/ConsoleProcess.ts +0 -59
  29. package/src/console/ConsoleStore.ts +0 -187
  30. package/src/console/ConsoleTracer.ts +0 -107
  31. package/src/console/Simulation.ts +0 -814
  32. package/src/console/console.html +0 -340
  33. package/src/console/index.ts +0 -3
  34. package/src/console/routes/errors/route.tsx +0 -97
  35. package/src/console/routes/fiberDetail.tsx +0 -54
  36. package/src/console/routes/fibers/route.tsx +0 -45
  37. package/src/console/routes/git/route.tsx +0 -64
  38. package/src/console/routes/layout.tsx +0 -4
  39. package/src/console/routes/logs/route.tsx +0 -77
  40. package/src/console/routes/metrics/route.tsx +0 -36
  41. package/src/console/routes/route.tsx +0 -8
  42. package/src/console/routes/routes/route.tsx +0 -30
  43. package/src/console/routes/services/route.tsx +0 -21
  44. package/src/console/routes/system/route.tsx +0 -43
  45. package/src/console/routes/traceDetail.tsx +0 -22
  46. package/src/console/routes/traces/route.tsx +0 -81
  47. package/src/console/routes/tree.ts +0 -30
  48. package/src/console/ui/Errors.tsx +0 -76
  49. package/src/console/ui/Fibers.tsx +0 -321
  50. package/src/console/ui/Git.tsx +0 -182
  51. package/src/console/ui/Logs.tsx +0 -46
  52. package/src/console/ui/Metrics.tsx +0 -78
  53. package/src/console/ui/Routes.tsx +0 -125
  54. package/src/console/ui/Services.tsx +0 -273
  55. package/src/console/ui/Shell.tsx +0 -62
  56. package/src/console/ui/System.tsx +0 -131
  57. package/src/console/ui/Traces.tsx +0 -426
  58. 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
- )