opencode-effect-enforcer 0.2.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,614 @@
1
+ ---
2
+ name: effect-batching
3
+ description: Implement automatic request batching and deduplication using Effect's Request, RequestResolver, and SqlResolver APIs. Use this skill when solving N+1 query problems, building batched data-fetching layers, or integrating request caching with resolvers.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in request batching, deduplication, and efficient data-fetching patterns.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
+ Browse and read files there directly to look up APIs, types, and implementations.
12
+
13
+ Reference this for:
14
+
15
+ - `Request` and `Request.Class` definitions (`packages/effect/src/Request.ts`)
16
+ - `RequestResolver` constructors and combinators (`packages/effect/src/RequestResolver.ts`)
17
+ - `SqlResolver` for SQL-specific batching (`packages/effect/src/unstable/sql/SqlResolver.ts`)
18
+ - Batching tutorial (`ai-docs/src/05_batching/10_request-resolver.ts`)
19
+
20
+ ## The N+1 Problem
21
+
22
+ Naive data fetching executes one query per item. Fetching 100 users by ID produces 100 separate queries. Effect's batching system solves this automatically: individual `Effect.request` calls made concurrently within a batch window are collected and resolved together in a single batch.
23
+
24
+ The key insight: calling code writes single-item lookups, but the runtime collects them and hands the resolver an array. No manual batching logic leaks into business code.
25
+
26
+ ## Select by Backend Capability
27
+
28
+ Use `RequestResolver` batching only when the backend can answer many distinct keys in one operation, such as SQL `IN (...)`, a DataLoader-style endpoint, or a batch GET API. The resolver should collapse a batch into fewer wire/database calls.
29
+
30
+ If the backend exposes only per-item endpoints, a resolver that loops over entries is not backend batching. Prefer `Effect.forEach(items, lookup, { concurrency: n })`, optionally with `Cache` for repeated-key memoization and in-flight deduplication. Use `RequestResolver.batchN` to respect a real batch endpoint's maximum request size, and `makeGrouped` when entries must be routed to different backend targets.
31
+
32
+ Selection guide:
33
+
34
+ - Repeated same key, concurrently or over time: `Cache`.
35
+ - Many distinct keys with a real batch endpoint: `Effect.request` + `RequestResolver`.
36
+ - Many distinct keys with per-item endpoints only: bounded `Effect.forEach`, optionally through `Cache`.
37
+
38
+ ## Request Definition
39
+
40
+ A `Request<Success, Error, Services>` describes a single lookup. Define requests using `Request.Class`:
41
+
42
+ ```typescript
43
+ import { Effect, Exit, Request, RequestResolver, Schema } from 'effect';
44
+
45
+ // Domain types
46
+ class User extends Schema.Class<User>('User')({
47
+ id: Schema.Number,
48
+ name: Schema.String,
49
+ email: Schema.String
50
+ }) {}
51
+
52
+ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
53
+ 'UserNotFound',
54
+ {
55
+ id: Schema.Number
56
+ }
57
+ ) {}
58
+
59
+ // Request definition using Request.Class
60
+ // Type params: { payload fields }, Success, Error, Services
61
+ class GetUserById extends Request.Class<
62
+ { readonly id: number },
63
+ User,
64
+ UserNotFound,
65
+ never
66
+ > {}
67
+ ```
68
+
69
+ ### Alternative: Interface + tagged constructor
70
+
71
+ For simpler cases or when you don't need a class:
72
+
73
+ ```typescript
74
+ interface GetUserById extends Request.Request<User, UserNotFound> {
75
+ readonly _tag: 'GetUserById';
76
+ readonly id: number;
77
+ }
78
+ const GetUserById = Request.tagged<GetUserById>('GetUserById');
79
+
80
+ // Usage:
81
+ const req = GetUserById({ id: 42 });
82
+ ```
83
+
84
+ ### Request equality
85
+
86
+ Requests use structural equality by default (via `Equal` trait). Two `GetUserById({ id: 1 })` instances are considered equal, enabling automatic deduplication within a batch window.
87
+
88
+ ## RequestResolver
89
+
90
+ A `RequestResolver<A>` handles batched execution of requests of type `A`. The resolver receives all collected requests as a `NonEmptyArray<Request.Entry<A>>` and must complete every entry.
91
+
92
+ ### Basic resolver with `RequestResolver.make`
93
+
94
+ ```typescript
95
+ const resolver = RequestResolver.make<GetUserById>(
96
+ Effect.fn(function* (entries) {
97
+ // `entries` is NonEmptyArray<Request.Entry<GetUserById>>
98
+ // Each entry has:
99
+ // - entry.request: the original request (e.g. { id: 1 })
100
+ // - entry.context: captured Context with request-scoped services
101
+ // - entry.completeUnsafe(exit): complete with Exit value
102
+
103
+ const ids = entries.map((e) => e.request.id);
104
+ const users = yield* fetchUsersByIds(ids); // single batched call
105
+
106
+ for (const entry of entries) {
107
+ const user = users.find((u) => u.id === entry.request.id);
108
+ if (user) {
109
+ entry.completeUnsafe(Exit.succeed(user));
110
+ } else {
111
+ entry.completeUnsafe(
112
+ Exit.fail(new UserNotFound({ id: entry.request.id }))
113
+ );
114
+ }
115
+ }
116
+ })
117
+ );
118
+ ```
119
+
120
+ ### Completing entries
121
+
122
+ Every entry in the batch MUST be completed. Failing to do so causes a `QueryFailure` error at runtime.
123
+
124
+ ```typescript
125
+ // Complete with success
126
+ entry.completeUnsafe(Exit.succeed(value));
127
+
128
+ // Complete with typed error
129
+ entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id })));
130
+
131
+ // Complete with defect
132
+ entry.completeUnsafe(Exit.die(new Error('unexpected')));
133
+ ```
134
+
135
+ ### Pure resolvers
136
+
137
+ For simple cases:
138
+
139
+ ```typescript
140
+ // Per-request pure function
141
+ const SquareResolver = RequestResolver.fromFunction<GetSquare>(
142
+ (entry) => entry.request.value * entry.request.value
143
+ );
144
+
145
+ // Batched pure function (results must match request order)
146
+ const DoubleResolver = RequestResolver.fromFunctionBatched<GetDouble>(
147
+ (entries) => entries.map((entry) => entry.request.value * 2)
148
+ );
149
+ ```
150
+
151
+ ### Per-request effectful resolver
152
+
153
+ When each request needs its own effect (no batching optimization, but still benefits from deduplication):
154
+
155
+ ```typescript
156
+ const UserResolver = RequestResolver.fromEffect<GetUserById>((entry) =>
157
+ Effect.gen(function* () {
158
+ const result = yield* httpClient.get(`/users/${entry.request.id}`);
159
+ return result;
160
+ })
161
+ );
162
+ ```
163
+
164
+ ### Tagged resolver (multiple request types)
165
+
166
+ Handle different request types in a single resolver:
167
+
168
+ ```typescript
169
+ type AppRequest = GetUser | GetPost;
170
+
171
+ const AppResolver = RequestResolver.fromEffectTagged<AppRequest>()({
172
+ GetUser: (entries) =>
173
+ Effect.succeed(entries.map((e) => `User ${e.request.id}`)),
174
+ GetPost: (entries) =>
175
+ Effect.succeed(entries.map((e) => `Post ${e.request.id}`))
176
+ });
177
+ ```
178
+
179
+ ### Grouped resolver
180
+
181
+ Group requests by a key so each group is resolved separately:
182
+
183
+ ```typescript
184
+ const resolver = RequestResolver.makeGrouped<GetUserByRole, string>({
185
+ key: ({ request }) => request.role,
186
+ resolver: (entries, role) =>
187
+ Effect.sync(() => {
188
+ console.log(
189
+ `Processing ${entries.length} requests for role: ${role}`
190
+ );
191
+ for (const entry of entries) {
192
+ entry.completeUnsafe(
193
+ Exit.succeed(`User ${entry.request.id} with role ${role}`)
194
+ );
195
+ }
196
+ })
197
+ });
198
+ ```
199
+
200
+ ## Using Requests with Effect.request
201
+
202
+ `Effect.request` connects a request instance to its resolver, returning a normal `Effect`:
203
+
204
+ ```typescript
205
+ const getUserById = (id: number) =>
206
+ Effect.request(new GetUserById({ id }), resolver);
207
+ ```
208
+
209
+ The resolver can also be an `Effect` that produces a resolver (useful when the resolver is constructed within a service layer):
210
+
211
+ ```typescript
212
+ const getUserById = (id: number) =>
213
+ Effect.request(new GetUserById({ id }), resolverEffect);
214
+ ```
215
+
216
+ ### Automatic batching
217
+
218
+ When multiple `Effect.request` calls run concurrently, they are automatically batched:
219
+
220
+ ```typescript
221
+ // These 5 lookups produce ONE call to the resolver
222
+ // Duplicate IDs (1, 2) are deduplicated
223
+ const result =
224
+ yield*
225
+ Effect.forEach([1, 2, 1, 3, 2], (id) => getUserById(id), {
226
+ concurrency: 'unbounded'
227
+ });
228
+ ```
229
+
230
+ ## Batch Window Configuration
231
+
232
+ ### `RequestResolver.setDelay`
233
+
234
+ Controls how long the resolver waits to collect requests before executing. More delay = larger batches but higher latency.
235
+
236
+ ```typescript
237
+ const resolver = RequestResolver.make<GetUserById>(/* ... */).pipe(
238
+ // Wait 10ms to collect more requests before flushing
239
+ RequestResolver.setDelay('10 millis')
240
+ );
241
+ ```
242
+
243
+ Default behavior (no `setDelay`): the resolver uses `Effect.yieldNow` which flushes after the current microtask, batching only requests that are already queued.
244
+
245
+ ### `RequestResolver.setDelayEffect`
246
+
247
+ For custom delay logic (e.g., logging, dynamic delays):
248
+
249
+ ```typescript
250
+ const resolver = pipe(
251
+ baseResolver,
252
+ RequestResolver.setDelayEffect(
253
+ Effect.gen(function* () {
254
+ yield* Effect.log('Waiting before processing batch...');
255
+ yield* Effect.sleep('50 millis');
256
+ })
257
+ )
258
+ );
259
+ ```
260
+
261
+ ### `RequestResolver.batchN`
262
+
263
+ Limit maximum batch size. Larger batches are split into multiple resolver calls:
264
+
265
+ ```typescript
266
+ const resolver = pipe(
267
+ baseResolver,
268
+ RequestResolver.batchN(100) // max 100 requests per batch
269
+ );
270
+ ```
271
+
272
+ ## Caching
273
+
274
+ ### `RequestResolver.withCache`
275
+
276
+ Adds an in-memory LRU or FIFO cache to a resolver. Cached requests skip the resolver entirely on subsequent lookups:
277
+
278
+ ```typescript
279
+ const resolver =
280
+ yield*
281
+ RequestResolver.make<GetUserById>(/* ... */).pipe(
282
+ RequestResolver.withCache({ capacity: 1024 })
283
+ // or: RequestResolver.withCache({ capacity: 1024, strategy: "fifo" })
284
+ );
285
+ ```
286
+
287
+ Note: `withCache` returns an `Effect<RequestResolver>` (it allocates mutable state), so use `yield*` when constructing.
288
+
289
+ Cache behavior:
290
+
291
+ - First lookup: request goes to resolver, result is cached
292
+ - Subsequent lookup for same request: served from cache immediately
293
+ - When capacity is exceeded, oldest entries are evicted (LRU or FIFO)
294
+ - In-flight deduplication: if the same request is pending, new callers attach to the pending result
295
+
296
+ ### `RequestResolver.asCache`
297
+
298
+ Converts a resolver into a `Cache` instance for more control (TTL, etc.):
299
+
300
+ ```typescript
301
+ const userCache =
302
+ yield*
303
+ pipe(
304
+ resolver,
305
+ RequestResolver.asCache({
306
+ capacity: 1024,
307
+ timeToLive: (exit, request) => '5 minutes'
308
+ })
309
+ );
310
+
311
+ // Cache operations are module functions in Effect v4
312
+ const user = yield* Cache.get(userCache, new GetUserById({ id: 1 }));
313
+ ```
314
+
315
+ ## Observability
316
+
317
+ ### `RequestResolver.withSpan`
318
+
319
+ Adds a tracing span around the resolver execution with automatic span links from each request's parent span:
320
+
321
+ ```typescript
322
+ const resolver = pipe(
323
+ baseResolver,
324
+ RequestResolver.withSpan('Users.getUserById.resolver')
325
+ );
326
+ ```
327
+
328
+ The span automatically includes a `batchSize` attribute and links to each request's parent span, giving full visibility into batching behavior in your tracing backend.
329
+
330
+ Combine with `Effect.withSpan` on the individual request for end-to-end traces:
331
+
332
+ ```typescript
333
+ const getUserById = (id: number) =>
334
+ Effect.request(new GetUserById({ id }), resolver).pipe(
335
+ Effect.withSpan('Users.getUserById', { attributes: { userId: id } })
336
+ );
337
+ ```
338
+
339
+ ### Accessing request services
340
+
341
+ Inside a resolver, each `Request.Entry` carries its captured `Context` with request-scoped services:
342
+
343
+ ```typescript
344
+ import { Context, Tracer } from 'effect';
345
+
346
+ const resolver = RequestResolver.make<GetUserById>(
347
+ Effect.fn(function* (entries) {
348
+ for (const entry of entries) {
349
+ const requestSpan = Context.getOption(
350
+ entry.context,
351
+ Tracer.ParentSpan
352
+ );
353
+ // ... use span for correlation
354
+ }
355
+ })
356
+ );
357
+ ```
358
+
359
+ ## Complete Service Pattern
360
+
361
+ The idiomatic pattern wraps request + resolver + caching inside a service layer:
362
+
363
+ ```typescript
364
+ import {
365
+ Effect,
366
+ Exit,
367
+ Layer,
368
+ Request,
369
+ RequestResolver,
370
+ Schema,
371
+ Context
372
+ } from 'effect';
373
+
374
+ class User extends Schema.Class<User>('User')({
375
+ id: Schema.Number,
376
+ name: Schema.String,
377
+ email: Schema.String
378
+ }) {}
379
+
380
+ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
381
+ 'UserNotFound',
382
+ {
383
+ id: Schema.Number
384
+ }
385
+ ) {}
386
+
387
+ class Users extends Context.Service<
388
+ Users,
389
+ {
390
+ getUserById(id: number): Effect.Effect<User, UserNotFound>;
391
+ }
392
+ >()('app/Users') {
393
+ static readonly layer = Layer.effect(
394
+ Users,
395
+ Effect.gen(function* () {
396
+ class GetUserById extends Request.Class<
397
+ { readonly id: number },
398
+ User,
399
+ UserNotFound,
400
+ never
401
+ > {}
402
+
403
+ const resolver = yield* RequestResolver.make<GetUserById>(
404
+ Effect.fn(function* (entries) {
405
+ const ids = entries.map((e) => e.request.id);
406
+ const users = yield* fetchBatch(ids);
407
+ for (const entry of entries) {
408
+ const user = users.find(
409
+ (u) => u.id === entry.request.id
410
+ );
411
+ entry.completeUnsafe(
412
+ user
413
+ ? Exit.succeed(user)
414
+ : Exit.fail(
415
+ new UserNotFound({
416
+ id: entry.request.id
417
+ })
418
+ )
419
+ );
420
+ }
421
+ })
422
+ ).pipe(
423
+ RequestResolver.setDelay('10 millis'),
424
+ RequestResolver.withSpan('Users.getUserById.resolver'),
425
+ RequestResolver.withCache({ capacity: 1024 })
426
+ );
427
+
428
+ const getUserById = (id: number) =>
429
+ Effect.request(new GetUserById({ id }), resolver).pipe(
430
+ Effect.withSpan('Users.getUserById', {
431
+ attributes: { userId: id }
432
+ })
433
+ );
434
+
435
+ return { getUserById } as const;
436
+ })
437
+ );
438
+ }
439
+ ```
440
+
441
+ ## SQL Integration with SqlResolver
442
+
443
+ `SqlResolver` (from `effect/unstable/sql`) provides schema-validated, batched SQL resolvers. Import:
444
+
445
+ ```typescript
446
+ import { SqlResolver } from 'effect/unstable/sql';
447
+ ```
448
+
449
+ ### SqlResolver.ordered
450
+
451
+ Results map 1:1 to requests in order. Errors if result count doesn't match:
452
+
453
+ ```typescript
454
+ const Insert = SqlResolver.ordered({
455
+ Request: Schema.String,
456
+ Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
457
+ execute: (names) =>
458
+ sql`INSERT INTO users ${sql.insert(names.map((name) => ({ name })))} RETURNING *`
459
+ });
460
+
461
+ const insertUser = SqlResolver.request(Insert);
462
+
463
+ // Batched: these two inserts become one SQL statement
464
+ const results =
465
+ yield*
466
+ Effect.all(
467
+ {
468
+ one: insertUser('alice'),
469
+ two: insertUser('bob')
470
+ },
471
+ { concurrency: 'unbounded' }
472
+ );
473
+ ```
474
+
475
+ ### SqlResolver.grouped
476
+
477
+ Returns multiple results per request, grouped by key:
478
+
479
+ ```typescript
480
+ const FindByName = SqlResolver.grouped({
481
+ Request: Schema.String,
482
+ RequestGroupKey: (name) => name,
483
+ Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
484
+ ResultGroupKey: (result) => result.name,
485
+ execute: (names) => sql`SELECT * FROM users WHERE name IN ${sql.in(names)}`
486
+ });
487
+
488
+ const findByName = SqlResolver.request(FindByName);
489
+ // Returns NonEmptyArray<User> or fails with NoSuchElementError
490
+ ```
491
+
492
+ ### SqlResolver.findById
493
+
494
+ Resolves single results by ID. Returns `NoSuchElementError` for missing entries:
495
+
496
+ ```typescript
497
+ const FindById = SqlResolver.findById({
498
+ Id: Schema.Number,
499
+ Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
500
+ ResultId: (result) => result.id,
501
+ execute: (ids) => sql`SELECT * FROM users WHERE id IN ${sql.in(ids)}`
502
+ });
503
+
504
+ const findById = SqlResolver.request(FindById);
505
+ // findById(1) => Effect<User, NoSuchElementError | SqlError | Schema.SchemaError>
506
+ ```
507
+
508
+ ### SqlResolver.void
509
+
510
+ For side-effect-only operations (inserts/updates with no return value):
511
+
512
+ ```typescript
513
+ const DeleteUser = SqlResolver.void({
514
+ Request: Schema.Number,
515
+ execute: (ids) => sql`DELETE FROM users WHERE id IN ${sql.in(ids)}`
516
+ });
517
+
518
+ const deleteUser = SqlResolver.request(DeleteUser);
519
+ ```
520
+
521
+ ### Transaction awareness
522
+
523
+ `SqlResolver` automatically groups requests by the transaction connection captured in each `Request.Entry.context`, so requests within a transaction are batched separately from those outside one. This depends on using the same `SqlClient` service instance that opened the transaction; requests executed with another client or a manually reserved connection do not join that transaction.
524
+
525
+ Encoding failures are completed as their underlying schema errors before batch execution. If every request in a batch fails encoding, the non-empty execute callback is not invoked; duplicate `findById` requests are all completed rather than surfacing a resolver-incomplete defect.
526
+
527
+ ## Resolver Combinators
528
+
529
+ ### `RequestResolver.around`
530
+
531
+ Execute setup/teardown around each batch:
532
+
533
+ ```typescript
534
+ const timedResolver = RequestResolver.around(
535
+ resolver,
536
+ (entries) => Effect.sync(() => Date.now()),
537
+ (entries, startTime) =>
538
+ Effect.log(
539
+ `Batch of ${entries.length} completed in ${Date.now() - startTime}ms`
540
+ )
541
+ );
542
+ ```
543
+
544
+ ### `RequestResolver.grouped`
545
+
546
+ Transform a resolver to group requests by a dynamic key:
547
+
548
+ ```typescript
549
+ const byDepartment = RequestResolver.grouped(
550
+ resolver,
551
+ ({ request }) => request.department
552
+ );
553
+ ```
554
+
555
+ ### `RequestResolver.race`
556
+
557
+ Race two resolvers, returning whichever completes first:
558
+
559
+ ```typescript
560
+ const fast = RequestResolver.race(cacheResolver, dbResolver);
561
+ ```
562
+
563
+ ## Common Anti-Patterns
564
+
565
+ ### WRONG: Completing only some entries
566
+
567
+ ```typescript
568
+ // BAD - entries without matches are never completed -> QueryFailure
569
+ const resolver = RequestResolver.make<GetUserById>(
570
+ Effect.fn(function* (entries) {
571
+ for (const entry of entries) {
572
+ const user = users.get(entry.request.id);
573
+ if (user) {
574
+ entry.completeUnsafe(Exit.succeed(user)); // What about misses?
575
+ }
576
+ }
577
+ })
578
+ );
579
+ ```
580
+
581
+ ### CORRECT: Always complete every entry
582
+
583
+ ```typescript
584
+ const resolver = RequestResolver.make<GetUserById>(
585
+ Effect.fn(function* (entries) {
586
+ for (const entry of entries) {
587
+ const user = users.get(entry.request.id);
588
+ entry.completeUnsafe(
589
+ user
590
+ ? Exit.succeed(user)
591
+ : Exit.fail(new UserNotFound({ id: entry.request.id }))
592
+ );
593
+ }
594
+ })
595
+ );
596
+ ```
597
+
598
+ ### WRONG: Using Effect.forEach without concurrency
599
+
600
+ ```typescript
601
+ // BAD - sequential execution, no batching occurs
602
+ yield* Effect.forEach([1, 2, 3], getUserById);
603
+ ```
604
+
605
+ ### CORRECT: Enable concurrency for batching
606
+
607
+ ```typescript
608
+ // GOOD - concurrent execution triggers batching
609
+ yield* Effect.forEach([1, 2, 3], getUserById, { concurrency: 'unbounded' });
610
+ ```
611
+
612
+ ### WRONG: Calling per-item endpoints from a "batched" resolver
613
+
614
+ This still makes N backend calls. Use bounded `Effect.forEach` directly and add `Cache` when repeated-key deduplication is useful. Introduce a resolver only after selecting a backend endpoint that truly accepts multiple keys.