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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-concurrency-testing
|
|
3
|
+
description: Test Effect concurrency primitives including PubSub, Deferred, Latch, Fiber coordination, SubscriptionRef, and Stream. Use this skill when testing concurrent effects, event-driven systems, or fiber coordination.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect Concurrency Testing Skill
|
|
7
|
+
|
|
8
|
+
This skill provides patterns for testing Effect's concurrency primitives: fibers, latches, deferreds, PubSub, SubscriptionRef, and streams.
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
|
|
12
|
+
**CRITICAL**: Choose the correct coordination primitive based on what you need to synchronize.
|
|
13
|
+
|
|
14
|
+
| Need | Use |
|
|
15
|
+
| --------------------------------- | ------------------------------------------------------------------------------------- |
|
|
16
|
+
| Simple fiber yield | `Effect.yieldNow` |
|
|
17
|
+
| Wait for subscriber ready | `Deferred.make()` + `Deferred.await` |
|
|
18
|
+
| Wait for stream element | `Latch.make()` + `Stream.tap(() => latch.open)` |
|
|
19
|
+
| Passive subscription registration | explicit readiness signal if possible; otherwise a tiny one-tick yield/sleep fallback |
|
|
20
|
+
| Time-dependent behavior | `TestClock.adjust` |
|
|
21
|
+
| Verify events published | `PubSub.subscribe` + `PubSub.takeUpTo` |
|
|
22
|
+
| Check fiber status | `fiber.pollUnsafe()` |
|
|
23
|
+
|
|
24
|
+
For `Stream.fromPubSub` subscription registration, prefer an explicit readiness signal when you control the stream. If the API offers no readiness hook and you only need registration to settle before publishing, a tiny `Effect.yieldNow()` or very short sleep is an acceptable last resort. Avoid broad polling or arbitrary delays.
|
|
25
|
+
|
|
26
|
+
## Fiber Coordination Patterns
|
|
27
|
+
|
|
28
|
+
### Effect.yieldNow - Simple Fiber Scheduling
|
|
29
|
+
|
|
30
|
+
Use `Effect.yieldNow` when you need to allow other fibers to execute. This is preferred over `TestClock.adjust` for non-time-dependent code.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { it } from '@effect/vitest';
|
|
34
|
+
import { Effect, Exit, Fiber, Latch } from 'effect';
|
|
35
|
+
|
|
36
|
+
it.effect('fiber polling with yieldNow', () =>
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const latch = yield* Latch.make();
|
|
39
|
+
|
|
40
|
+
const fiber = yield* latch.await.pipe(Effect.forkChild);
|
|
41
|
+
|
|
42
|
+
yield* Effect.yieldNow();
|
|
43
|
+
|
|
44
|
+
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
45
|
+
|
|
46
|
+
yield* latch.open;
|
|
47
|
+
|
|
48
|
+
expect(yield* fiber.await).toEqual(Exit.void);
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Latch - Explicit Coordination
|
|
54
|
+
|
|
55
|
+
`Latch.make()` creates a gate that blocks fibers until opened:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { it } from '@effect/vitest';
|
|
59
|
+
import { Effect, Fiber, Latch } from 'effect';
|
|
60
|
+
|
|
61
|
+
it.effect('latch coordination', () =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const latch = yield* Latch.make();
|
|
64
|
+
|
|
65
|
+
const fiber = yield* Effect.gen(function* () {
|
|
66
|
+
yield* latch.await;
|
|
67
|
+
return 'completed';
|
|
68
|
+
}).pipe(Effect.forkChild);
|
|
69
|
+
|
|
70
|
+
yield* Effect.yieldNow();
|
|
71
|
+
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
72
|
+
|
|
73
|
+
yield* latch.open;
|
|
74
|
+
|
|
75
|
+
const result = yield* Fiber.join(fiber);
|
|
76
|
+
expect(result).toBe('completed');
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Latch Operations
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { Latch } from 'effect';
|
|
85
|
+
|
|
86
|
+
declare const latch: Latch.Latch;
|
|
87
|
+
|
|
88
|
+
latch.await; // Wait until latch is open
|
|
89
|
+
latch.open; // Open the latch (current and future waiters proceed)
|
|
90
|
+
latch.close; // Close the latch (future waiters suspend again)
|
|
91
|
+
latch.release; // Wake current waiters only; latch stays closed for future waiters
|
|
92
|
+
latch.whenOpen; // Run effect only when latch is open
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Deferred - Signal Readiness Between Fibers
|
|
96
|
+
|
|
97
|
+
Use `Deferred` when one fiber needs to signal another with a value:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { it } from '@effect/vitest';
|
|
101
|
+
import { Effect, Deferred, Fiber } from 'effect';
|
|
102
|
+
|
|
103
|
+
it.effect('deferred signaling', () =>
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
const signal = yield* Deferred.make<number>();
|
|
106
|
+
|
|
107
|
+
const consumer = yield* Effect.gen(function* () {
|
|
108
|
+
const value = yield* Deferred.await(signal);
|
|
109
|
+
return value * 2;
|
|
110
|
+
}).pipe(Effect.forkChild);
|
|
111
|
+
|
|
112
|
+
yield* Deferred.succeed(signal, 21);
|
|
113
|
+
|
|
114
|
+
const result = yield* Fiber.join(consumer);
|
|
115
|
+
expect(result).toBe(42);
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### fiber.pollUnsafe() - Check Completion Without Blocking
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Exit, Fiber } from 'effect';
|
|
124
|
+
|
|
125
|
+
declare const fiber: Fiber.Fiber<string>;
|
|
126
|
+
|
|
127
|
+
fiber.pollUnsafe();
|
|
128
|
+
// Returns undefined if running
|
|
129
|
+
// Returns Exit<A, E> if completed (success, failure, or interrupted)
|
|
130
|
+
|
|
131
|
+
// Check if still running
|
|
132
|
+
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
133
|
+
|
|
134
|
+
// Check if completed
|
|
135
|
+
expect(fiber.pollUnsafe()).toBeDefined();
|
|
136
|
+
|
|
137
|
+
// Check specific completion
|
|
138
|
+
expect(fiber.pollUnsafe()).toEqual(Exit.succeed('result'));
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## PubSub Event Testing
|
|
142
|
+
|
|
143
|
+
### Direct Event Verification
|
|
144
|
+
|
|
145
|
+
Use `Effect.scoped` to manage PubSub subscription lifecycle:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { it } from '@effect/vitest';
|
|
149
|
+
import { Effect, PubSub } from 'effect';
|
|
150
|
+
|
|
151
|
+
it.effect('verify published events', () =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
const pubsub = yield* PubSub.unbounded<string>();
|
|
154
|
+
|
|
155
|
+
yield* Effect.scoped(
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const sub = yield* PubSub.subscribe(pubsub);
|
|
158
|
+
|
|
159
|
+
yield* PubSub.publish(pubsub, 'event-1');
|
|
160
|
+
yield* PubSub.publish(pubsub, 'event-2');
|
|
161
|
+
|
|
162
|
+
const events = yield* PubSub.takeAll(sub);
|
|
163
|
+
|
|
164
|
+
expect(events).toEqual(['event-1', 'event-2']);
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Both events are published **before** `PubSub.takeAll`, so it returns immediately. `PubSub.takeAll` suspends when the subscription is empty and always returns a `NonEmptyArray`; for non-blocking drains or “no more events” assertions, use `PubSub.takeUpTo(sub, n)` instead, which returns whatever is buffered (possibly an empty array).
|
|
172
|
+
|
|
173
|
+
### Testing Event Publishers
|
|
174
|
+
|
|
175
|
+
When testing a service that publishes events:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { it } from '@effect/vitest';
|
|
179
|
+
import { Effect, PubSub, Context, Layer } from 'effect';
|
|
180
|
+
|
|
181
|
+
interface UserEvent {
|
|
182
|
+
readonly type: 'created' | 'deleted';
|
|
183
|
+
readonly userId: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class EventBus extends Context.Service<EventBus, PubSub.PubSub<UserEvent>>()(
|
|
187
|
+
'EventBus'
|
|
188
|
+
) {}
|
|
189
|
+
|
|
190
|
+
class UserService extends Context.Service<
|
|
191
|
+
UserService,
|
|
192
|
+
{ readonly createUser: (id: string) => Effect.Effect<void> }
|
|
193
|
+
>()('UserService') {}
|
|
194
|
+
|
|
195
|
+
declare const UserServiceLive: Layer.Layer<UserService, never, EventBus>;
|
|
196
|
+
|
|
197
|
+
it.effect('should publish user created event', () =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
const pubsub = yield* PubSub.unbounded<UserEvent>();
|
|
200
|
+
|
|
201
|
+
yield* Effect.scoped(
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
const sub = yield* PubSub.subscribe(pubsub);
|
|
204
|
+
|
|
205
|
+
const service = yield* UserService;
|
|
206
|
+
yield* service.createUser('user-123');
|
|
207
|
+
|
|
208
|
+
const events = yield* PubSub.takeAll(sub);
|
|
209
|
+
|
|
210
|
+
expect(events).toHaveLength(1);
|
|
211
|
+
expect(events[0]).toEqual({
|
|
212
|
+
type: 'created',
|
|
213
|
+
userId: 'user-123'
|
|
214
|
+
});
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
}).pipe(
|
|
218
|
+
Effect.provide(UserServiceLive),
|
|
219
|
+
Effect.provide(Layer.succeed(EventBus, pubsub))
|
|
220
|
+
)
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Concurrent Publisher/Subscriber Testing
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { it } from '@effect/vitest';
|
|
228
|
+
import { Deferred, Effect, PubSub, Fiber, Latch, Array as A } from 'effect';
|
|
229
|
+
|
|
230
|
+
it.effect('concurrent publishers and subscribers', () =>
|
|
231
|
+
Effect.gen(function* () {
|
|
232
|
+
const values = A.range(0, 9);
|
|
233
|
+
const latch = yield* Latch.make();
|
|
234
|
+
const ready = yield* Deferred.make<void>();
|
|
235
|
+
const pubsub = yield* PubSub.bounded<number>(10);
|
|
236
|
+
|
|
237
|
+
const subscriber = yield* PubSub.subscribe(pubsub).pipe(
|
|
238
|
+
Effect.flatMap((sub) =>
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
// Signal that the subscription is registered before publishing
|
|
241
|
+
yield* Deferred.succeed(ready, undefined);
|
|
242
|
+
yield* latch.await;
|
|
243
|
+
return yield* Effect.forEach(values, () =>
|
|
244
|
+
PubSub.take(sub)
|
|
245
|
+
);
|
|
246
|
+
})
|
|
247
|
+
),
|
|
248
|
+
Effect.scoped,
|
|
249
|
+
Effect.forkScoped
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Wait until the subscriber has actually subscribed (PubSub is not
|
|
253
|
+
// an event log — publishing before subscription would drop messages)
|
|
254
|
+
yield* Deferred.await(ready);
|
|
255
|
+
|
|
256
|
+
yield* PubSub.publishAll(pubsub, values);
|
|
257
|
+
yield* latch.open;
|
|
258
|
+
|
|
259
|
+
const result = yield* Fiber.join(subscriber);
|
|
260
|
+
expect(result).toEqual(values);
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## SubscriptionRef Testing
|
|
266
|
+
|
|
267
|
+
### Testing Stream Changes with Latches
|
|
268
|
+
|
|
269
|
+
The latch pattern ensures the stream subscription is ready before mutations:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { it } from '@effect/vitest';
|
|
273
|
+
import { Effect, Fiber, Latch, Number } from 'effect';
|
|
274
|
+
import { Stream, SubscriptionRef } from 'effect';
|
|
275
|
+
|
|
276
|
+
it.effect('multiple subscribers can receive changes', () =>
|
|
277
|
+
Effect.gen(function* () {
|
|
278
|
+
const ref = yield* SubscriptionRef.make(0);
|
|
279
|
+
const latch1 = yield* Latch.make();
|
|
280
|
+
const latch2 = yield* Latch.make();
|
|
281
|
+
|
|
282
|
+
const fiber1 = yield* SubscriptionRef.changes(ref).pipe(
|
|
283
|
+
Stream.tap(() => latch1.open),
|
|
284
|
+
Stream.take(3),
|
|
285
|
+
Stream.runCollect,
|
|
286
|
+
Effect.forkScoped
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
yield* latch1.await;
|
|
290
|
+
yield* SubscriptionRef.update(ref, Number.increment);
|
|
291
|
+
|
|
292
|
+
const fiber2 = yield* SubscriptionRef.changes(ref).pipe(
|
|
293
|
+
Stream.tap(() => latch2.open),
|
|
294
|
+
Stream.take(2),
|
|
295
|
+
Stream.runCollect,
|
|
296
|
+
Effect.forkScoped
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
yield* latch2.await;
|
|
300
|
+
yield* SubscriptionRef.update(ref, Number.increment);
|
|
301
|
+
|
|
302
|
+
const result1 = yield* Fiber.join(fiber1);
|
|
303
|
+
const result2 = yield* Fiber.join(fiber2);
|
|
304
|
+
|
|
305
|
+
expect(result1).toEqual([0, 1, 2]);
|
|
306
|
+
expect(result2).toEqual([1, 2]);
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Testing Subscription Interruption
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { it } from '@effect/vitest';
|
|
315
|
+
import { Effect, Exit, Fiber, Latch, Number } from 'effect';
|
|
316
|
+
import { Pull, Stream, SubscriptionRef } from 'effect';
|
|
317
|
+
|
|
318
|
+
it.effect('subscriptions are interruptible', () =>
|
|
319
|
+
Effect.gen(function* () {
|
|
320
|
+
const ref = yield* SubscriptionRef.make(0);
|
|
321
|
+
const latch = yield* Latch.make();
|
|
322
|
+
|
|
323
|
+
const fiber = yield* SubscriptionRef.changes(ref).pipe(
|
|
324
|
+
Stream.tap(() => latch.open),
|
|
325
|
+
Stream.take(10),
|
|
326
|
+
Stream.runCollect,
|
|
327
|
+
Effect.forkScoped
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
yield* latch.await;
|
|
331
|
+
yield* SubscriptionRef.update(ref, Number.increment);
|
|
332
|
+
yield* Fiber.interrupt(fiber);
|
|
333
|
+
|
|
334
|
+
const result = yield* Fiber.await(fiber);
|
|
335
|
+
|
|
336
|
+
expect(Exit.isFailure(result) && Pull.isDoneCause(result.cause)).toBe(
|
|
337
|
+
true
|
|
338
|
+
);
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Stream Testing
|
|
344
|
+
|
|
345
|
+
### Collecting Stream Results
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { it } from '@effect/vitest';
|
|
349
|
+
import { Effect } from 'effect';
|
|
350
|
+
import { Stream } from 'effect';
|
|
351
|
+
|
|
352
|
+
it.effect('should collect stream elements', () =>
|
|
353
|
+
Effect.gen(function* () {
|
|
354
|
+
const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
|
|
355
|
+
Stream.filter((n) => n % 2 === 0),
|
|
356
|
+
Stream.runCollect
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
expect(result).toEqual([2, 4]);
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Testing Stream Side Effects
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { it } from '@effect/vitest';
|
|
368
|
+
import { Effect, Ref } from 'effect';
|
|
369
|
+
import { Stream } from 'effect';
|
|
370
|
+
|
|
371
|
+
it.effect('should track side effects', () =>
|
|
372
|
+
Effect.gen(function* () {
|
|
373
|
+
const log = yield* Ref.make<string[]>([]);
|
|
374
|
+
|
|
375
|
+
yield* Stream.make('a', 'b', 'c').pipe(
|
|
376
|
+
Stream.tap((item) => Ref.update(log, (items) => [...items, item])),
|
|
377
|
+
Stream.runDrain
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const logged = yield* Ref.get(log);
|
|
381
|
+
expect(logged).toEqual(['a', 'b', 'c']);
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Testing Stream Errors
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { it } from '@effect/vitest';
|
|
390
|
+
import { Effect, Exit, Schema } from 'effect';
|
|
391
|
+
import { Stream } from 'effect';
|
|
392
|
+
|
|
393
|
+
class StreamError extends Schema.TaggedError<StreamError>()(
|
|
394
|
+
'StreamError',
|
|
395
|
+
{
|
|
396
|
+
message: Schema.String
|
|
397
|
+
}
|
|
398
|
+
) {}
|
|
399
|
+
|
|
400
|
+
it.effect('should handle stream errors', () =>
|
|
401
|
+
Effect.gen(function* () {
|
|
402
|
+
const result = yield* Stream.make(1, 2, 3).pipe(
|
|
403
|
+
Stream.mapEffect((n) =>
|
|
404
|
+
n === 2
|
|
405
|
+
? Effect.fail(new StreamError({ message: 'boom' }))
|
|
406
|
+
: Effect.succeed(n)
|
|
407
|
+
),
|
|
408
|
+
Stream.runCollect,
|
|
409
|
+
Effect.exit
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(Exit.isFailure(result)).toBe(true);
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Testing Stream Finalization
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { it } from '@effect/vitest';
|
|
421
|
+
import { Effect, Ref } from 'effect';
|
|
422
|
+
import { Stream } from 'effect';
|
|
423
|
+
|
|
424
|
+
it.effect('should run finalizers', () =>
|
|
425
|
+
Effect.gen(function* () {
|
|
426
|
+
const finalized = yield* Ref.make(false);
|
|
427
|
+
|
|
428
|
+
yield* Stream.make(1, 2, 3).pipe(
|
|
429
|
+
Stream.ensuring(Ref.set(finalized, true)),
|
|
430
|
+
Stream.take(1),
|
|
431
|
+
Stream.runDrain
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
expect(yield* Ref.get(finalized)).toBe(true);
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Interruption Testing
|
|
440
|
+
|
|
441
|
+
### Testing Fiber Interruption
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { it } from '@effect/vitest';
|
|
445
|
+
import { Effect, Exit, Fiber, Cause } from 'effect';
|
|
446
|
+
|
|
447
|
+
it.effect('should handle interruption', () =>
|
|
448
|
+
Effect.gen(function* () {
|
|
449
|
+
const fiber = yield* Effect.never.pipe(Effect.forkChild);
|
|
450
|
+
|
|
451
|
+
yield* Fiber.interrupt(fiber);
|
|
452
|
+
|
|
453
|
+
const result = yield* Fiber.await(fiber);
|
|
454
|
+
|
|
455
|
+
expect(Exit.hasInterrupts(result)).toBe(true);
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Testing Interrupted-Only Cause
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import { it } from '@effect/vitest';
|
|
464
|
+
import { Effect, Exit, Fiber, Cause } from 'effect';
|
|
465
|
+
|
|
466
|
+
it.effect('should have interrupted-only cause', () =>
|
|
467
|
+
Effect.gen(function* () {
|
|
468
|
+
const fiber = yield* Effect.never.pipe(Effect.forkChild);
|
|
469
|
+
|
|
470
|
+
yield* Fiber.interrupt(fiber);
|
|
471
|
+
|
|
472
|
+
const result = yield* Fiber.await(fiber);
|
|
473
|
+
|
|
474
|
+
expect(
|
|
475
|
+
Exit.isFailure(result) && Cause.hasInterruptsOnly(result.cause)
|
|
476
|
+
).toBe(true);
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Time-Dependent Concurrency Testing
|
|
482
|
+
|
|
483
|
+
Use `TestClock` only when testing time-dependent behavior like delays, timeouts, or schedules.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import { it } from '@effect/vitest';
|
|
487
|
+
import { Effect, Fiber, Duration } from 'effect';
|
|
488
|
+
import { TestClock } from 'effect/testing';
|
|
489
|
+
|
|
490
|
+
it.effect('should handle delayed concurrent operations', () =>
|
|
491
|
+
Effect.gen(function* () {
|
|
492
|
+
const fiber = yield* Effect.gen(function* () {
|
|
493
|
+
yield* Effect.sleep(Duration.seconds(5));
|
|
494
|
+
return 'done';
|
|
495
|
+
}).pipe(Effect.forkChild);
|
|
496
|
+
|
|
497
|
+
yield* TestClock.adjust(Duration.seconds(5));
|
|
498
|
+
|
|
499
|
+
const result = yield* Fiber.join(fiber);
|
|
500
|
+
expect(result).toBe('done');
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Anti-Patterns
|
|
506
|
+
|
|
507
|
+
### DON'T use TestClock for non-time-dependent code
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { Effect, Duration } from 'effect';
|
|
511
|
+
import { TestClock } from 'effect/testing';
|
|
512
|
+
|
|
513
|
+
// BAD - Using TestClock when not needed
|
|
514
|
+
Effect.gen(function* () {
|
|
515
|
+
const fiber = yield* someEffect.pipe(Effect.forkChild);
|
|
516
|
+
yield* TestClock.adjust(Duration.millis(100));
|
|
517
|
+
yield* Fiber.join(fiber);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// GOOD - Use yieldNow for simple yielding
|
|
521
|
+
Effect.gen(function* () {
|
|
522
|
+
const fiber = yield* someEffect.pipe(Effect.forkChild);
|
|
523
|
+
yield* Effect.yieldNow();
|
|
524
|
+
yield* Fiber.join(fiber);
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### DON'T poll in a loop without yieldNow
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { Effect, Fiber } from 'effect';
|
|
532
|
+
|
|
533
|
+
declare const fiber: Fiber.Fiber<void>;
|
|
534
|
+
|
|
535
|
+
// BAD - Busy loop
|
|
536
|
+
while (fiber.pollUnsafe() === undefined) {
|
|
537
|
+
// Spins forever!
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// GOOD - Yield between polls or use Fiber.await
|
|
541
|
+
Effect.gen(function* () {
|
|
542
|
+
while (fiber.pollUnsafe() === undefined) {
|
|
543
|
+
yield* Effect.yieldNow();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// BETTER - Just await the fiber
|
|
548
|
+
Effect.gen(function* () {
|
|
549
|
+
yield* Fiber.await(fiber);
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### DON'T forget Effect.scoped for PubSub subscriptions
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
import { Effect, PubSub } from 'effect';
|
|
557
|
+
|
|
558
|
+
declare const pubsub: PubSub.PubSub<string>;
|
|
559
|
+
|
|
560
|
+
// BAD - Subscription leaks
|
|
561
|
+
Effect.gen(function* () {
|
|
562
|
+
const sub = yield* PubSub.subscribe(pubsub);
|
|
563
|
+
// Sub is never cleaned up!
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// GOOD - Scoped subscription
|
|
567
|
+
Effect.gen(function* () {
|
|
568
|
+
yield* Effect.scoped(
|
|
569
|
+
Effect.gen(function* () {
|
|
570
|
+
const sub = yield* PubSub.subscribe(pubsub);
|
|
571
|
+
// takeUpTo never suspends; takeAll would hang here (nothing published)
|
|
572
|
+
const events = yield* PubSub.takeUpTo(sub, 10);
|
|
573
|
+
// Sub cleaned up when scope closes
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### DON'T start subscriptions after mutations
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import { Effect, Fiber } from 'effect';
|
|
583
|
+
import { Stream, SubscriptionRef } from 'effect';
|
|
584
|
+
|
|
585
|
+
declare const ref: SubscriptionRef.SubscriptionRef<number>;
|
|
586
|
+
|
|
587
|
+
// BAD - May miss events: the subscription starts AFTER the first mutation
|
|
588
|
+
Effect.gen(function* () {
|
|
589
|
+
yield* SubscriptionRef.update(ref, (n) => n + 1);
|
|
590
|
+
|
|
591
|
+
const fiber = yield* SubscriptionRef.changes(ref).pipe(
|
|
592
|
+
Stream.take(1),
|
|
593
|
+
Stream.runCollect,
|
|
594
|
+
Effect.forkChild
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
yield* SubscriptionRef.update(ref, (n) => n + 1);
|
|
598
|
+
|
|
599
|
+
const result = yield* Fiber.join(fiber);
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Quality Checklist
|
|
604
|
+
|
|
605
|
+
- [ ] Using correct coordination primitive for the use case
|
|
606
|
+
- [ ] `Effect.scoped` wraps PubSub subscriptions
|
|
607
|
+
- [ ] Latches ensure stream subscriptions are ready before mutations
|
|
608
|
+
- [ ] `Effect.yieldNow` used instead of TestClock for non-time-dependent code
|
|
609
|
+
- [ ] Fiber interruption tested with `Exit.hasInterrupts` or `Cause.hasInterruptsOnly`
|
|
610
|
+
- [ ] Stream finalizers verified with `Stream.ensuring`
|
|
611
|
+
- [ ] No busy polling without yields
|
|
612
|
+
- [ ] Test is deterministic (no race conditions)
|