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,668 @@
1
+ ---
2
+ name: effect-parallelization
3
+ description: Run Effect computations concurrently with Effect.all/forEach concurrency options, racing combinators (race, raceAll, raceFirst), and coordination primitives (Semaphore, PartitionedSemaphore, Latch). Use when fanning out work over a collection, limiting parallelism, racing alternatives for first-success, enforcing per-key or global rate limits, or gating fibers on a startup signal.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in declarative concurrency — `Effect.all`, `Effect.forEach`, racing, and the coordination primitives `Semaphore`, `PartitionedSemaphore`, and `Latch`.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — Effect v4 differs substantially from v3 and from most training data.
11
+
12
+ Key files:
13
+
14
+ - `packages/effect/src/Effect.ts` — `all`, `forEach`, `race`/`raceAll`/`raceFirst`/`raceAllFirst`, `firstSuccessOf`, `timeout`/`timeoutOption`/`timeoutOrElse`, `zip`/`zipWith`, `filter`/`filterMap`/`filterMapEffect`, `partition`, `validate`, `findFirst`, and `replicate`/`replicateEffect` (very large file — grep for the export, then read its JSDoc)
15
+ - `packages/effect/src/Types.ts` — the `Concurrency` type (`number | "unbounded"`)
16
+ - `packages/effect/src/Semaphore.ts` — counting semaphore: `make`, `withPermit(s)`, `withPermitsIfAvailable`, `take`/`release`/`releaseAll`, `resize`
17
+ - `packages/effect/src/PartitionedSemaphore.ts` — keyed permit pool with round-robin fairness across partitions
18
+ - `packages/effect/src/Latch.ts` — open/closed gate: `make`, `open`, `close`, `release`, `await`, `whenOpen`
19
+ - `packages/effect/src/internal/effect.ts` — the actual implementations (`forEachConcurrent`, `raceAll`, ...) when you need exact semantics
20
+ - `packages/effect/test/Effect.test.ts` — `forEach`/`all`/`partition`/`validate`/`raceAll` describe blocks with interruption edge cases
21
+ - `packages/effect/test/Semaphore.test.ts`, `test/PartitionedSemaphore.test.ts`, `test/Latch.test.ts` — real usage with `TestClock`
22
+ - `migration/v3-to-v4.md` — rename map (e.g. `Effect.makeSemaphore` → `Semaphore.make`)
23
+
24
+ ## Core Model
25
+
26
+ Concurrency in Effect is declarative: combinators that operate on many effects take a `concurrency` option, fork child fibers internally, and uphold structured concurrency — when the combined effect finishes, fails, or is interrupted, every in-flight child fiber is interrupted before the result is produced.
27
+
28
+ ```ts
29
+ // Types.Concurrency
30
+ type Concurrency = number | 'unbounded';
31
+ ```
32
+
33
+ | Value | Meaning |
34
+ | --- | --- |
35
+ | omitted | Sequential (concurrency 1). **This is the default.** |
36
+ | `n: number` | At most `n` effects run at once (values < 1 are clamped to 1) |
37
+ | `'unbounded'` | All effects start at once |
38
+
39
+ The two central signatures:
40
+
41
+ ```ts
42
+ // Combine a fixed structure of effects (tuple, array, iterable, or record)
43
+ Effect.all(arg, options?: {
44
+ concurrency?: Concurrency;
45
+ discard?: boolean; // true => Effect<void>
46
+ mode?: 'default' | 'result'; // 'result' => never fails, each slot is a Result<A, E>
47
+ });
48
+
49
+ // Apply an effectful function over an Iterable
50
+ Effect.forEach(elements, (a, index) => Effect<B, E, R>, options?: {
51
+ concurrency?: Concurrency;
52
+ discard?: boolean;
53
+ });
54
+
55
+ // Standalone data-last form
56
+ Effect.forEach((a, index) => Effect<B, E, R>, options?)(elements);
57
+ ```
58
+
59
+ Imports used throughout this skill (all from the stable `effect` barrel):
60
+
61
+ ```ts
62
+ import { Effect, Latch, PartitionedSemaphore, Result, Semaphore } from 'effect';
63
+ ```
64
+
65
+ For manual fiber control (`Effect.forkChild`, `Fiber.join`, `FiberSet`, ...) see the `effect-fiber` skill. For pipelines over values produced over time, see the `effect-stream` skill.
66
+
67
+ ---
68
+
69
+ ## 1. The `concurrency` Option
70
+
71
+ Every collection combinator (`all`, `forEach`, `filter`, `partition`, `validate`, `replicateEffect`, `filterMapEffect`) accepts the same option. Without it, execution is **sequential**:
72
+
73
+ ```ts
74
+ // Sequential — one at a time (the default!)
75
+ yield* Effect.forEach(ids, fetchUser);
76
+
77
+ // At most 8 in flight
78
+ yield* Effect.forEach(ids, fetchUser, { concurrency: 8 });
79
+
80
+ // All at once
81
+ yield* Effect.forEach(ids, fetchUser, { concurrency: 'unbounded' });
82
+ ```
83
+
84
+ As of beta.102, `"inherit"`, `References.CurrentConcurrency`, and `Effect.withConcurrency` are removed. Reusable APIs that expose fan-out policy should accept a `Types.Concurrency` value and pass it explicitly to each combinator instead of relying on ambient configuration.
85
+
86
+ ### Failure semantics under concurrency
87
+
88
+ The default mode is fail-fast: on the first failure, in-flight siblings are interrupted, not-yet-started items are skipped, and the combined effect fails. If several concurrent effects fail before interruption lands, their failure reasons are merged into a single `Cause`. Results are always collected by input index, so success order matches input order regardless of completion order (exceptions: `filter` and `filterMapEffect`, see section 6).
89
+
90
+ ---
91
+
92
+ ## 2. `Effect.all` — Tuples, Arrays, Records, Iterables
93
+
94
+ The output shape follows the input shape, with types tracked precisely:
95
+
96
+ ```ts
97
+ // Tuple — heterogeneous, result is a typed tuple
98
+ const [n, s] = yield* Effect.all([Effect.succeed(42), Effect.succeed('hi')]);
99
+ // [number, string]
100
+
101
+ // Record — results collected under the same keys
102
+ const page = yield* Effect.all(
103
+ {
104
+ user: fetchUser(id),
105
+ posts: fetchPosts(id),
106
+ ads: fetchAds()
107
+ },
108
+ { concurrency: 'unbounded' }
109
+ );
110
+ // { user: User; posts: Post[]; ads: Ad[] }
111
+
112
+ // Any iterable (Array, Set, generator, ...) — result is an Array
113
+ const results = yield* Effect.all(new Set([eff1, eff2, eff3]));
114
+ ```
115
+
116
+ ### `discard: true` — run for effects only
117
+
118
+ ```ts
119
+ yield* Effect.all([logA, logB, logC], { concurrency: 'unbounded', discard: true });
120
+ // Effect<void, E, R>
121
+ ```
122
+
123
+ ### `mode: 'result'` — run everything, never fail
124
+
125
+ Each slot becomes a `Result<A, E>` and the error channel becomes `never`. Every effect runs to completion (no fail-fast interruption):
126
+
127
+ ```ts
128
+ const results = yield* Effect.all(
129
+ [mightFail1, mightFail2, mightFail3],
130
+ { mode: 'result', concurrency: 'unbounded' }
131
+ );
132
+ // [Result<A1, E1>, Result<A2, E2>, Result<A3, E3>]
133
+
134
+ const successes = results.filter(Result.isSuccess).map((r) => r.success);
135
+ ```
136
+
137
+ `mode: 'result'` works for records too — each value becomes a `Result`. The v3 modes `'either'` and `'validate'` no longer exist; `Result` replaced `Either` in v4, and validate-style accumulation lives in `Effect.validate` (section 6).
138
+
139
+ ### Replication
140
+
141
+ ```ts
142
+ // Array of n identical effects (not yet run) — feed to a combinator
143
+ const effects = Effect.replicate(pingServer, 5);
144
+ yield* Effect.all(effects, { concurrency: 'unbounded' });
145
+
146
+ // Run an effect n times with Effect.all semantics
147
+ const samples = yield* Effect.replicateEffect(measureLatency, 10, {
148
+ concurrency: 'unbounded'
149
+ });
150
+ // Effect<Array<A>, E, R>; supports { discard: true } as well
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 3. `Effect.forEach` — Effectful Iteration
156
+
157
+ The workhorse for fan-out over a work list. The callback receives the element and its index:
158
+
159
+ ```ts
160
+ const enriched = yield* Effect.forEach(
161
+ orders,
162
+ (order, index) => enrichOrder(order),
163
+ { concurrency: 8 }
164
+ );
165
+ // Array<EnrichedOrder> — in input order
166
+
167
+ // Standalone data-last usage is supported; the callback determines A.
168
+ const fetchUsers = Effect.forEach(fetchUser, { concurrency: 8 });
169
+ const users = yield* fetchUsers(userIds);
170
+
171
+ // Side effects only
172
+ yield* Effect.forEach(events, publishEvent, {
173
+ concurrency: 4,
174
+ discard: true
175
+ });
176
+ ```
177
+
178
+ Notes:
179
+
180
+ - Works on any `Iterable` — including strings (`Effect.forEach('abc', f)` iterates characters).
181
+ - Results are written by index: input order is preserved even with `concurrency: 'unbounded'`.
182
+ - Sequential mode (no option) stops at the first failure without starting later elements; concurrent mode interrupts in-flight siblings on failure.
183
+ - Interrupting the parent fiber interrupts all in-flight children; items beyond the concurrency window that were never forked simply never run.
184
+ - Concurrent `Effect.request` calls inside `forEach` trigger automatic batching — see the `effect-batching` skill.
185
+
186
+ For an unbounded or infinite source, do not collect into an array — use `Stream.fromIterable(...).pipe(Stream.mapEffect(f, { concurrency }))` instead (see the `effect-stream` skill).
187
+
188
+ ---
189
+
190
+ ## 4. Concurrent Zips
191
+
192
+ `Effect.zip` and `Effect.zipWith` combine exactly two effects. Their option key is `concurrent: boolean` — **not** `concurrency`:
193
+
194
+ ```ts
195
+ // Sequential by default
196
+ const pair = yield* Effect.zip(task1, task2);
197
+ // [A, B]
198
+
199
+ // Run both at once
200
+ const pair2 = yield* Effect.zip(task1, task2, { concurrent: true });
201
+
202
+ const combined = yield* Effect.zipWith(
203
+ fetchPrice,
204
+ fetchQuantity,
205
+ (price, qty) => price * qty,
206
+ { concurrent: true }
207
+ );
208
+ ```
209
+
210
+ `{ concurrent: true }` is implemented as `Effect.all([self, that], { concurrency: 2 })`, so it inherits fail-fast semantics: if one side fails, the other is interrupted.
211
+
212
+ The v3 `Effect.zipRight`/`Effect.zipLeft` are renamed: use `Effect.andThen` / `Effect.tap`.
213
+
214
+ ---
215
+
216
+ ## 5. Racing
217
+
218
+ Four combinators, two axes: two effects vs. many, and first-*success* vs. first-*completion*:
219
+
220
+ | | First success wins | First completion wins (even failure) |
221
+ | --- | --- | --- |
222
+ | Two effects | `Effect.race(a, b)` | `Effect.raceFirst(a, b)` |
223
+ | Iterable | `Effect.raceAll(effects)` | `Effect.raceAllFirst(effects)` |
224
+
225
+ ```ts
226
+ // First successful replica wins; losers are interrupted
227
+ const fastest = yield* Effect.raceAll(replicaUrls.map(queryReplica));
228
+
229
+ // First completion settles the race — a fast failure loses you the result
230
+ const settled = yield* Effect.raceFirst(primary, secondary);
231
+ ```
232
+
233
+ Semantics (verified in `internal/effect.ts`):
234
+
235
+ - **Loser interruption is awaited**: when a winner settles, the remaining fibers are interrupted uninterruptibly and the race only resumes with the winning exit after those interruptions (including finalizers) complete.
236
+ - `race`/`raceAll`: early failures do not finish the race — the race keeps waiting until one effect succeeds or all have failed. If all fail, the failure reasons are collected into one combined `Cause`.
237
+ - `raceFirst`/`raceAllFirst`: the first fiber to settle (success *or* failure) decides the outcome.
238
+ - Effects are forked in iteration order; if an early effect completes synchronously, later effects may never start at all.
239
+ - All four accept an optional `onWinner` callback for observing the winning fiber:
240
+
241
+ ```ts
242
+ Effect.raceAll(candidates, {
243
+ onWinner: ({ fiber, index, parentFiber }) => {
244
+ console.log(`candidate ${index} won`);
245
+ }
246
+ });
247
+ ```
248
+
249
+ ### Sequential fallback: `firstSuccessOf`
250
+
251
+ `Effect.firstSuccessOf` is **not** a race — it tries effects one at a time, in order, returning the first success. Later effects never start if an earlier one succeeds. If all fail, it fails with the *last* error; an empty iterable is a defect.
252
+
253
+ ```ts
254
+ const config = yield* Effect.firstSuccessOf([
255
+ readFromEnv,
256
+ readFromFile,
257
+ Effect.succeed(defaultConfig)
258
+ ]);
259
+ ```
260
+
261
+ ### Timeouts are races against the clock
262
+
263
+ `Effect.timeout(effect, '5 seconds')` (typed `Cause.TimeoutError` failure), `Effect.timeoutOption` (`Option.none` on timeout), and `Effect.timeoutOrElse` all interrupt the source effect when the timer wins — same structured guarantees as racing.
264
+
265
+ ---
266
+
267
+ ## 6. Concurrent Filtering, Partitioning, Validation
268
+
269
+ ### `Effect.filter` — keep elements passing a predicate
270
+
271
+ Accepts a plain predicate/refinement, or an effectful predicate with a `concurrency` option:
272
+
273
+ ```ts
274
+ // Sync predicate
275
+ const evens = yield* Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0);
276
+
277
+ // Effectful predicate, concurrent
278
+ const reachable = yield* Effect.filter(
279
+ hosts,
280
+ (host) => pingHost(host),
281
+ { concurrency: 10 }
282
+ );
283
+ ```
284
+
285
+ **Order caveat**: with `concurrency > 1`, `filter` and `filterMapEffect` collect kept values in *completion* order, not input order (they push on completion rather than writing by index).
286
+
287
+ ### `Effect.filterMap` / `Effect.filterMapEffect` — filter + transform
288
+
289
+ These take a `Filter` (a function returning `Result.succeed(b)` to keep-and-transform or `Result.fail(x)` to skip). `filterMap` is synchronous; `filterMapEffect` is effectful with a `concurrency` option (note: its callback receives only the element, no index):
290
+
291
+ ```ts
292
+ const strong = yield* Effect.filterMapEffect(
293
+ candidates,
294
+ (c) =>
295
+ Effect.map(score(c), (s) =>
296
+ s > 0.8 ? Result.succeed({ ...c, score: s }) : Result.fail(s)
297
+ ),
298
+ { concurrency: 4 }
299
+ );
300
+ // Array of kept, transformed values
301
+ ```
302
+
303
+ ### `Effect.partition` — split failures from successes, never fail
304
+
305
+ Runs every element (no short-circuit). Returns `[excluded, satisfying]` — failures first. Both arrays preserve input order:
306
+
307
+ ```ts
308
+ const [failures, users] = yield* Effect.partition(
309
+ userIds,
310
+ (id) => fetchUser(id),
311
+ { concurrency: 8 }
312
+ );
313
+ // Effect<[excluded: Array<E>, satisfying: Array<User>], never, R>
314
+ ```
315
+
316
+ ### `Effect.validate` — accumulate all failures
317
+
318
+ Like `partition`, but fails with a `NonEmptyArray<E>` of every failure if at least one element failed; succeeds with all results otherwise. Supports `{ discard: true }`:
319
+
320
+ ```ts
321
+ const validated = yield* Effect.validate(
322
+ formFields,
323
+ (field) => validateField(field),
324
+ { concurrency: 'unbounded' }
325
+ );
326
+ // Effect<Array<Valid>, NonEmptyArray<FieldError>, R>
327
+ ```
328
+
329
+ ### `Effect.findFirst` / `Effect.findFirstFilter` — sequential short-circuit search
330
+
331
+ ```ts
332
+ const firstHealthy = yield* Effect.findFirst(servers, (s) => checkHealth(s));
333
+ // Effect<Option<Server>, E, R> — stops at the first match, always sequential
334
+ ```
335
+
336
+ `findFirstFilter` is the transforming variant: the callback returns `Effect<Result<B, X>>` and the first `Result.succeed` short-circuits with `Option.some(b)`.
337
+
338
+ ---
339
+
340
+ ## 7. `Semaphore` — Bounded Access to Shared Resources
341
+
342
+ A counting semaphore from the `effect/Semaphore` module (exported from the `effect` barrel). Unlike a `concurrency` option — which bounds one call site — a semaphore bounds access *across* call sites and fibers.
343
+
344
+ ```ts
345
+ const program = Effect.gen(function* () {
346
+ const sem = yield* Semaphore.make(4); // 4 permits
347
+ // or synchronously, outside Effect: Semaphore.makeUnsafe(4)
348
+
349
+ yield* Effect.forEach(
350
+ jobs,
351
+ (job) => sem.withPermit(processJob(job)),
352
+ { concurrency: 'unbounded', discard: true }
353
+ );
354
+ });
355
+ ```
356
+
357
+ ### Instance API
358
+
359
+ ```ts
360
+ sem.withPermit(effect); // acquire 1 permit, run, release on exit
361
+ sem.withPermits(2)(effect); // weighted — note: curried!
362
+ sem.withPermitsIfAvailable(1)(effect); // Effect<Option<A>, E, R> — Option.none if permits unavailable, no waiting
363
+ sem.take(2); // Effect<number> — manual acquire (waits; see fairness note below); returns acquired count
364
+ sem.takeIfAvailable(2); // Effect<boolean> — acquire immediately or return false
365
+ sem.release(2); // Effect<number> — manual release; returns resulting free permits
366
+ sem.releaseAll; // Effect<number> — return every taken permit
367
+ sem.resize(8); // Effect<void> — change total permits in place
368
+ ```
369
+
370
+ ### Module-level duals
371
+
372
+ Every operation also exists as a module function, usable data-first or in pipes:
373
+
374
+ ```ts
375
+ yield* Semaphore.withPermit(sem, criticalSection);
376
+ yield* Semaphore.withPermits(sem, 2, heavyTask);
377
+ yield* heavyTask.pipe(Semaphore.withPermits(sem, 2));
378
+ yield* Semaphore.withPermitsIfAvailable(sem, 1, optionalWork); // Option<A>
379
+ yield* Semaphore.takeIfAvailable(sem, 2); // boolean; manual non-blocking acquire
380
+ yield* Semaphore.resize(sem, 8);
381
+ ```
382
+
383
+ Guarantees and gotchas (verified in source/tests):
384
+
385
+ - `withPermit*` releases permits on success, failure, *and* interruption — acquisition and release are wrapped in `uninterruptibleMask`.
386
+ - Pending `take`s are woken in arrival order, but a waiter requesting more permits than are currently free is skipped while later, smaller requests proceed — a `take(1)` can overtake a blocked `take(3)`. Strict FIFO holds only when all requests use the same permit count (e.g. `withPermit`).
387
+ - `take(n)`/`release(n)` are a low-level protocol: an unbalanced `release` inflates the permit count; an interrupted fiber between `take` and `release` leaks permits. Prefer `withPermits`.
388
+ - Requesting more permits than the total never completes (unless the semaphore is later `resize`d up).
389
+ - `resize` can shrink below the currently-taken count; existing holders keep their permits and new acquisitions wait until enough are released.
390
+ - `Semaphore.make(1)` is the idiomatic mutex for serializing access to mutable state.
391
+
392
+ ---
393
+
394
+ ## 8. `PartitionedSemaphore` — Keyed Fairness over a Shared Pool
395
+
396
+ A `PartitionedSemaphore<K>` shares one permit pool across many partition keys, but tracks waiters *per key* and distributes released permits across waiting partitions in **round-robin** order. Use it when independent groups (tenants, hosts, queues) compete for the same bounded resource and a busy group must not starve the others. A plain `Semaphore` serves its single arrival-order queue regardless of key; the partitioned variant interleaves: `p1, p2, p1, p2, ...`.
397
+
398
+ ```ts
399
+ const program = Effect.gen(function* () {
400
+ const sem = yield* PartitionedSemaphore.make<string>({ permits: 10 });
401
+
402
+ const handle = (tenantId: string, req: Request) =>
403
+ sem.withPermit(tenantId)(processRequest(req));
404
+
405
+ // Weighted variant
406
+ const handleBig = (tenantId: string, req: Request) =>
407
+ sem.withPermits(tenantId, 3)(processBigRequest(req));
408
+ });
409
+ ```
410
+
411
+ ### API surface
412
+
413
+ ```ts
414
+ PartitionedSemaphore.make<K>({ permits: number }); // Effect<PartitionedSemaphore<K>>
415
+ PartitionedSemaphore.makeUnsafe<K>({ permits }); // synchronous
416
+
417
+ sem.withPermit(key)(effect); // 1 permit for this key
418
+ sem.withPermits(key, n)(effect); // n permits — curried, like Semaphore
419
+ sem.withPermitsIfAvailable(n)(effect); // NOT keyed — Option<A>, no waiting
420
+ sem.take(key, n); // Effect<void> — manual, keyed
421
+ sem.release(n); // Effect<number> — manual, not keyed
422
+ sem.available; // Effect<number> — free permits (snapshot)
423
+ sem.capacity; // number — fixed total
424
+
425
+ // Module-level duals exist for all of the above:
426
+ yield* PartitionedSemaphore.withPermits(sem, 'tenant-a', 2, task);
427
+ yield* task.pipe(PartitionedSemaphore.withPermit(sem, 'tenant-a'));
428
+ ```
429
+
430
+ Gotchas (verified in source/tests):
431
+
432
+ - **Requesting more permits than `capacity` never completes** — the take resolves to `Effect.never`, silently hanging the fiber.
433
+ - Zero or negative permit requests run the effect immediately without acquiring anything.
434
+ - Non-finite `permits` (e.g. `Infinity`) creates an unbounded semaphore where every operation is a no-op pass-through; negative capacities are clamped to 0.
435
+ - Interruption while waiting returns any partially-acquired permits to the pool — no leaks.
436
+ - `withPermitsIfAvailable` takes no key: it only checks the shared pool.
437
+
438
+ ---
439
+
440
+ ## 9. `Latch` — Gating Fibers on a Signal
441
+
442
+ A `Latch` is a reusable open/closed gate. Closed: `await` and `whenOpen` suspend. Open: they pass through immediately. Unlike `Deferred` (one-shot, carries a value — see the `effect-fiber` skill), a latch is value-less and can be closed and reopened any number of times.
443
+
444
+ ```ts
445
+ const program = Effect.gen(function* () {
446
+ const ready = yield* Latch.make(); // starts CLOSED; Latch.make(true) starts open
447
+ // or synchronously: Latch.makeUnsafe(false)
448
+
449
+ // Workers block until the latch opens
450
+ const worker = (id: number) =>
451
+ Effect.gen(function* () {
452
+ yield* ready.await; // suspends while closed
453
+ yield* Effect.log(`worker ${id} running`);
454
+ });
455
+
456
+ const fibers = yield* Effect.forEach([1, 2, 3], (id) =>
457
+ Effect.forkChild(worker(id))
458
+ );
459
+
460
+ yield* loadConfiguration;
461
+ yield* ready.open; // releases all current AND future waiters
462
+ });
463
+ ```
464
+
465
+ ### Operations
466
+
467
+ ```ts
468
+ latch.await; // Effect<void> — suspend until open (or released)
469
+ latch.open; // Effect<boolean> — open; wake current + future waiters; true if state changed
470
+ latch.close; // Effect<boolean> — future waiters suspend again; true if state changed
471
+ latch.release; // Effect<boolean> — wake CURRENT waiters only; latch stays closed
472
+ latch.whenOpen(effect); // run effect once the latch allows passage
473
+ latch.openUnsafe(); // synchronous variants for non-Effect code
474
+ latch.closeUnsafe();
475
+
476
+ // Module-level equivalents
477
+ yield* Latch.open(latch);
478
+ yield* Latch.await(latch);
479
+ yield* Latch.whenOpen(latch, effect);
480
+ ```
481
+
482
+ `open` vs `release`: `open` flips the state so all future `await`s pass immediately; `release` is a one-shot pulse — current waiters proceed, the latch remains closed, and the next waiter suspends again. Use close/open pairs to implement pause/resume:
483
+
484
+ ```ts
485
+ const running = yield* Latch.make(true); // open = running
486
+
487
+ // In a polling loop:
488
+ const step = running.whenOpen(pollOnce);
489
+
490
+ // Elsewhere: pause and resume
491
+ yield* running.close;
492
+ yield* running.open;
493
+ ```
494
+
495
+ ---
496
+
497
+ ## 10. Structured Concurrency Guarantees
498
+
499
+ All combinators in this skill uphold the same invariants:
500
+
501
+ 1. **No leaked fibers**: children forked by `all`/`forEach`/`race*`/`zip { concurrent: true }` cannot outlive the combinator. Completion, failure, or interruption of the parent interrupts all in-flight children first.
502
+ 2. **Fail-fast with cleanup**: in default mode, the first failure interrupts siblings; their finalizers (`Effect.ensuring`, `acquireRelease` releases) run before the combined effect settles.
503
+ 3. **Interruption propagates**: interrupting the fiber running `Effect.forEach(..., { concurrency: 8 })` interrupts the 8 in-flight workers and skips the rest.
504
+ 4. **Deterministic results**: `all`/`forEach`/`partition`/`validate` order results by input index regardless of completion order.
505
+
506
+ When you need to *escape* structure — background fibers, daemons, fiber handles — that is `Effect.forkChild` / `forkScoped` / `forkDetach` territory: see the `effect-fiber` skill. To test concurrent code deterministically with `TestClock`, see the `effect-concurrency-testing` skill.
507
+
508
+ ---
509
+
510
+ ## Key Patterns
511
+
512
+ ### Bounded fan-out with error partitioning
513
+
514
+ Process a work list with a concurrency cap; collect failures without aborting the batch:
515
+
516
+ ```ts
517
+ import { Effect } from 'effect';
518
+
519
+ const syncAllUsers = (userIds: ReadonlyArray<string>) =>
520
+ Effect.gen(function* () {
521
+ const [failures, synced] = yield* Effect.partition(
522
+ userIds,
523
+ (id) => syncUser(id),
524
+ { concurrency: 8 }
525
+ );
526
+ if (failures.length > 0) {
527
+ yield* Effect.log(`${failures.length} of ${userIds.length} failed`);
528
+ }
529
+ return synced;
530
+ });
531
+ ```
532
+
533
+ ### First-success-wins with sequential fallback
534
+
535
+ Race the fast replicas concurrently; only if all of them fail, try the expensive cold standby:
536
+
537
+ ```ts
538
+ const fetchQuote = Effect.firstSuccessOf([
539
+ Effect.raceAll(replicaUrls.map((url) => queryReplica(url))),
540
+ queryColdStandby
541
+ ]);
542
+ ```
543
+
544
+ ### Hedged requests
545
+
546
+ Start a backup request only if the primary has not answered within 200ms; whichever succeeds first wins and the loser is interrupted:
547
+
548
+ ```ts
549
+ const hedged = Effect.race(
550
+ queryPrimary,
551
+ Effect.delay(queryBackup, '200 millis')
552
+ );
553
+ ```
554
+
555
+ ### Global rate limit shared across call sites
556
+
557
+ A `concurrency` option only bounds one combinator call. To bound a resource globally (DB pool, external API), put a semaphore in the service and wrap every operation:
558
+
559
+ ```ts
560
+ import { Context, Effect, Layer, Semaphore } from 'effect';
561
+
562
+ class GeoApi extends Context.Service<
563
+ GeoApi,
564
+ { geocode(address: string): Effect.Effect<Coords, GeoError> }
565
+ >()('app/GeoApi') {
566
+ static readonly layer = Layer.effect(
567
+ GeoApi,
568
+ Effect.gen(function* () {
569
+ const sem = yield* Semaphore.make(5); // provider allows 5 in-flight
570
+ const geocode = (address: string) =>
571
+ sem.withPermit(callProvider(address));
572
+ return { geocode } as const;
573
+ })
574
+ );
575
+ }
576
+
577
+ // Callers can use any concurrency they like — at most 5 hit the provider
578
+ const geocodeMany = (addresses: ReadonlyArray<string>) =>
579
+ Effect.gen(function* () {
580
+ const { geocode } = yield* GeoApi;
581
+ return yield* Effect.forEach(addresses, geocode, {
582
+ concurrency: 'unbounded'
583
+ });
584
+ });
585
+ ```
586
+
587
+ ### Per-tenant concurrency with cross-tenant fairness
588
+
589
+ ```ts
590
+ import { Effect, PartitionedSemaphore } from 'effect';
591
+
592
+ const makeIngestor = Effect.gen(function* () {
593
+ // 16 workers total, shared by all tenants; released permits rotate
594
+ // round-robin across tenants with queued work
595
+ const sem = yield* PartitionedSemaphore.make<string>({ permits: 16 });
596
+
597
+ const ingest = (tenantId: string, batch: ReadonlyArray<Event>) =>
598
+ sem.withPermit(tenantId)(writeBatch(tenantId, batch));
599
+
600
+ return { ingest } as const;
601
+ });
602
+ ```
603
+
604
+ ### Coordinated startup with a latch
605
+
606
+ Fork workers eagerly, but hold them at a gate until initialization completes:
607
+
608
+ ```ts
609
+ import { Effect, Latch } from 'effect';
610
+
611
+ const main = Effect.gen(function* () {
612
+ const ready = yield* Latch.make(); // closed
613
+
614
+ yield* Effect.forEach(
615
+ queueNames,
616
+ (name) => Effect.forkChild(ready.whenOpen(consumeQueue(name))),
617
+ { discard: true }
618
+ );
619
+
620
+ yield* runMigrations;
621
+ yield* warmCaches;
622
+ yield* ready.open; // all consumers start together
623
+ });
624
+ ```
625
+
626
+ ### Throttled batch processing
627
+
628
+ Combine chunking with bounded concurrency — at most 4 batches in flight, each batch written atomically:
629
+
630
+ ```ts
631
+ import { Array as Arr, Effect } from 'effect';
632
+
633
+ const writeAll = (rows: ReadonlyArray<Row>) =>
634
+ Effect.forEach(
635
+ Arr.chunksOf(rows, 100),
636
+ (batch) => insertBatch(batch),
637
+ { concurrency: 4, discard: true }
638
+ );
639
+ ```
640
+
641
+ ### Validate everything, report every error
642
+
643
+ ```ts
644
+ const checkConfig = (entries: ReadonlyArray<Entry>) =>
645
+ Effect.validate(entries, validateEntry, { concurrency: 'unbounded' }).pipe(
646
+ Effect.mapError((errors) => new ConfigInvalid({ errors }))
647
+ );
648
+ ```
649
+
650
+ ---
651
+
652
+ ## Common Mistakes
653
+
654
+ 1. **Assuming `Effect.all` / `Effect.forEach` are parallel by default** — they are sequential. Pass `{ concurrency: n | 'unbounded' }` explicitly; without it you also silently lose request batching (see effect-batching).
655
+ 2. **Using removed ambient concurrency APIs** — beta.102 removed `"inherit"`, `References.CurrentConcurrency`, and `Effect.withConcurrency`. Pass a number or `"unbounded"` explicitly at each combinator.
656
+ 3. **Wrong option key on zips** — `Effect.zip`/`zipWith` take `{ concurrent: true }` (boolean), not `{ concurrency: ... }`.
657
+ 4. **v3 `mode: 'either'` / `mode: 'validate'` on `Effect.all`** — gone. v4 has `mode: 'result'` (slots become `Result<A, E>`); for accumulate-all-failures use `Effect.validate`, which fails with `NonEmptyArray<E>`.
658
+ 5. **`Effect.makeSemaphore` / `Effect.makeLatch` no longer exist** — they moved to their own modules: `Semaphore.make(n)`, `Latch.make(open?)`, both importable from `'effect'`.
659
+ 6. **Expecting `firstSuccessOf` to race** — it is strictly sequential (and fails with only the *last* error). For concurrent first-success with loser interruption, use `Effect.raceAll`.
660
+ 7. **Confusing `race` with `raceFirst`** — `race`/`raceAll` ignore failures until something succeeds (or everything fails); `raceFirst`/`raceAllFirst` settle on the first completion, so a fast failure wins the race and fails the whole thing.
661
+ 8. **Relying on output order from concurrent `Effect.filter`/`filterMapEffect`** — they collect in completion order. `forEach`, `all`, `partition`, and `validate` preserve input order; the filters do not.
662
+ 9. **Calling `sem.withPermits(2, effect)` on the instance** — instance `withPermits(n)` is curried: `sem.withPermits(2)(effect)`. Only the module-level `Semaphore.withPermits(sem, 2, effect)` takes the effect as a third argument. Same for `PartitionedSemaphore`.
663
+ 10. **Manual `take`/`release` instead of `withPermits`** — an interrupt between `take` and `release` leaks permits and an extra `release` inflates the pool. `withPermit(s)` is interruption-safe.
664
+ 11. **Requesting more permits than capacity** — on `PartitionedSemaphore` this resolves to `Effect.never` (silent hang); on `Semaphore` it waits forever unless someone calls `resize`. Validate weights against capacity.
665
+ 12. **Using `latch.release` to open a latch** — `release` only wakes the *current* waiters and leaves the latch closed; the next `await` suspends again. Use `latch.open` to let future waiters through.
666
+ 13. **Hand-rolling `Promise.allSettled` semantics** — don't wrap exits manually; `Effect.all(..., { mode: 'result' })` or `Effect.partition` already run everything and surface per-item outcomes.
667
+ 14. **Using v3 fork names (`Effect.fork`, `forkDaemon`)** — see the effect-fiber skill for the v4 equivalents.
668
+ 15. **Fanning out an unbounded source through `forEach`** — `forEach` materializes the iterable into an array under concurrency. For large or infinite inputs use `Stream.mapEffect(f, { concurrency })` (effect-stream skill) to keep backpressure.