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