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,384 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-pubsub-event-bus
|
|
3
|
+
description: Build typed event buses using Effect PubSub and Stream. Use this skill when implementing publish/subscribe communication between services, replacing callback-based event systems, or building reactive service architectures with typed event streams.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PubSub Event Bus with Effect v4
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Effect's `PubSub` module provides a typed, composable publish/subscribe primitive. Combined with `Stream.fromPubSub` and `Effect.forkScoped`, it replaces imperative callback-based event buses with a fully typed, stream-based subscription model where cleanup is automatic.
|
|
11
|
+
|
|
12
|
+
**When to use this skill:**
|
|
13
|
+
|
|
14
|
+
- Building intra-service communication (event bus, message broker)
|
|
15
|
+
- Replacing callback-based event systems with typed streams
|
|
16
|
+
- Implementing reactive patterns where services subscribe to domain events
|
|
17
|
+
- Managing per-instance event channels with scoped cleanup
|
|
18
|
+
|
|
19
|
+
## Import Pattern
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Effect, PubSub, Stream } from 'effect';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core Pattern: Typed Event Bus Service
|
|
26
|
+
|
|
27
|
+
Define events as a discriminated union, then build a Bus service that publishes and subscribes using `PubSub`:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Effect, Layer, PubSub, Schema, Context, Stream } from 'effect';
|
|
31
|
+
|
|
32
|
+
// ─── Event Definitions ──────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
class FileChanged extends Schema.TaggedClass<FileChanged>()('FileChanged', {
|
|
35
|
+
path: Schema.String,
|
|
36
|
+
kind: Schema.Literals(['created', 'modified', 'deleted'])
|
|
37
|
+
}) {}
|
|
38
|
+
|
|
39
|
+
class ConfigReloaded extends Schema.TaggedClass<ConfigReloaded>()(
|
|
40
|
+
'ConfigReloaded',
|
|
41
|
+
{
|
|
42
|
+
source: Schema.String
|
|
43
|
+
}
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
type BusEvent = FileChanged | ConfigReloaded;
|
|
47
|
+
|
|
48
|
+
// ─── Bus Service ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export namespace Bus {
|
|
51
|
+
export interface Interface {
|
|
52
|
+
readonly publish: (event: BusEvent) => Effect.Effect<void>;
|
|
53
|
+
readonly subscribe: <T extends BusEvent>(
|
|
54
|
+
eventClass: new (...args: ReadonlyArray<never>) => T
|
|
55
|
+
) => Stream.Stream<T>;
|
|
56
|
+
readonly subscribeAll: Stream.Stream<BusEvent>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class Service extends Context.Service<Service, Interface>()(
|
|
60
|
+
'@app/Bus'
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
export const layer = Layer.effect(
|
|
64
|
+
Service,
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const pubsub = yield* PubSub.unbounded<BusEvent>();
|
|
67
|
+
|
|
68
|
+
// Cleanup: shutdown PubSub when scope closes
|
|
69
|
+
yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub));
|
|
70
|
+
|
|
71
|
+
const publish = Effect.fn('Bus.publish')(function* (
|
|
72
|
+
event: BusEvent
|
|
73
|
+
) {
|
|
74
|
+
yield* PubSub.publish(pubsub, event);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const subscribe = <T extends BusEvent>(
|
|
78
|
+
eventClass: new (...args: ReadonlyArray<never>) => T
|
|
79
|
+
): Stream.Stream<T> =>
|
|
80
|
+
Stream.fromPubSub(pubsub).pipe(
|
|
81
|
+
Stream.filter((evt): evt is T => evt instanceof eventClass)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const subscribeAll = Stream.fromPubSub(pubsub);
|
|
85
|
+
|
|
86
|
+
return Service.of({ publish, subscribe, subscribeAll });
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Subscribing to Events
|
|
93
|
+
|
|
94
|
+
Consumers use `Stream.fromPubSub` (via the Bus service) + `Effect.forkScoped` to register subscriptions that are automatically cleaned up when the scope closes:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Effect, Stream } from 'effect';
|
|
98
|
+
|
|
99
|
+
const setupFileWatcher = Effect.gen(function* () {
|
|
100
|
+
const bus = yield* Bus.Service;
|
|
101
|
+
|
|
102
|
+
// Fork a scoped fiber that processes FileChanged events
|
|
103
|
+
yield* bus.subscribe(FileChanged).pipe(
|
|
104
|
+
Stream.filter((evt) => evt.path.endsWith('.ts')),
|
|
105
|
+
Stream.runForEach((evt) =>
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
yield* Effect.logInfo(
|
|
108
|
+
`File changed: ${evt.path} (${evt.kind})`
|
|
109
|
+
);
|
|
110
|
+
yield* reloadModule(evt.path);
|
|
111
|
+
})
|
|
112
|
+
),
|
|
113
|
+
Effect.forkScoped
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Key points:
|
|
119
|
+
|
|
120
|
+
- **`Effect.forkScoped`** ties the subscription fiber's lifetime to the enclosing scope — no explicit unsubscribe needed
|
|
121
|
+
- **Stream combinators** (`filter`, `map`, `debounce`, `groupBy`) compose naturally before `runForEach`
|
|
122
|
+
- **No `acquireRelease` bookkeeping** — the stream and its fiber are cleaned up automatically
|
|
123
|
+
|
|
124
|
+
### Why `forkScoped` and not `forkChild`
|
|
125
|
+
|
|
126
|
+
`Effect.forkScoped` ties the fiber to the `Scope` lifecycle, while `Effect.forkChild` ties it to the parent fiber. For subscriptions registered during service construction (inside `Layer.effect`), `forkScoped` is correct because the subscription must live as long as the layer's scope, not the constructing fiber.
|
|
127
|
+
|
|
128
|
+
## Publishing Events
|
|
129
|
+
|
|
130
|
+
Publishing is straightforward — call `PubSub.publish` or use the Bus service:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const onFileChange = Effect.fn('Watcher.onFileChange')(function* (
|
|
134
|
+
path: string,
|
|
135
|
+
kind: 'created' | 'modified' | 'deleted'
|
|
136
|
+
) {
|
|
137
|
+
const bus = yield* Bus.Service;
|
|
138
|
+
yield* bus.publish(new FileChanged({ path, kind }));
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Pattern: Per-Type PubSub Channels
|
|
143
|
+
|
|
144
|
+
For high-throughput systems, maintain a `Map` of per-type PubSub channels to avoid filtering overhead on the wildcard channel:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { Effect, PubSub, Stream } from 'effect';
|
|
148
|
+
|
|
149
|
+
interface Payload {
|
|
150
|
+
readonly type: string;
|
|
151
|
+
readonly data: unknown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const make = Effect.gen(function* () {
|
|
155
|
+
const wildcard = yield* PubSub.unbounded<Payload>();
|
|
156
|
+
const typed = new Map<string, PubSub.PubSub<Payload>>();
|
|
157
|
+
|
|
158
|
+
yield* Effect.addFinalizer(() =>
|
|
159
|
+
Effect.gen(function* () {
|
|
160
|
+
yield* PubSub.shutdown(wildcard);
|
|
161
|
+
for (const ps of typed.values()) {
|
|
162
|
+
yield* PubSub.shutdown(ps);
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const getOrCreate = (type: string) =>
|
|
168
|
+
Effect.gen(function* () {
|
|
169
|
+
const existing = typed.get(type);
|
|
170
|
+
if (existing) return existing;
|
|
171
|
+
const ps = yield* PubSub.unbounded<Payload>();
|
|
172
|
+
typed.set(type, ps);
|
|
173
|
+
return ps;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const publish = Effect.fn('Bus.publish')(function* (event: Payload) {
|
|
177
|
+
yield* PubSub.publish(wildcard, event);
|
|
178
|
+
const ps = typed.get(event.type);
|
|
179
|
+
if (ps) yield* PubSub.publish(ps, event);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const subscribe = (type: string): Stream.Stream<Payload> =>
|
|
183
|
+
Stream.unwrap(
|
|
184
|
+
getOrCreate(type).pipe(Effect.map((ps) => Stream.fromPubSub(ps)))
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const subscribeAll = Stream.fromPubSub(wildcard);
|
|
188
|
+
|
|
189
|
+
return { publish, subscribe, subscribeAll };
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Pattern: Graceful Shutdown Event
|
|
194
|
+
|
|
195
|
+
Publish a final event before shutting down PubSub channels so subscribers can perform cleanup:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
yield*
|
|
199
|
+
Effect.addFinalizer(() =>
|
|
200
|
+
Effect.gen(function* () {
|
|
201
|
+
// Notify all subscribers that the bus is shutting down
|
|
202
|
+
yield* PubSub.publish(
|
|
203
|
+
wildcard,
|
|
204
|
+
new InstanceDisposed({ reason: 'scope-closed' })
|
|
205
|
+
);
|
|
206
|
+
// Then shut down the channel
|
|
207
|
+
yield* PubSub.shutdown(wildcard);
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Subscribers can detect this event and perform teardown:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
yield*
|
|
216
|
+
bus.subscribeAll.pipe(
|
|
217
|
+
Stream.takeUntil((evt) => evt instanceof InstanceDisposed),
|
|
218
|
+
Stream.runForEach(handleEvent),
|
|
219
|
+
Effect.forkScoped
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Redis Pub/Sub
|
|
224
|
+
|
|
225
|
+
For cross-process pub/sub, use the portable `Redis.Redis` service rather than modeling Redis as an in-memory `PubSub`. `redis.subscribe(channel)` is scoped and returns a dequeue whose values contain both `channel` and `message`.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { Effect, Stream } from 'effect';
|
|
229
|
+
import { Redis } from 'effect/unstable/persistence';
|
|
230
|
+
|
|
231
|
+
declare const handleRedisMessage: (
|
|
232
|
+
channel: string,
|
|
233
|
+
message: string
|
|
234
|
+
) => Effect.Effect<void>;
|
|
235
|
+
|
|
236
|
+
const consumeRedisEvents = Effect.gen(function* () {
|
|
237
|
+
const redis = yield* Redis.Redis;
|
|
238
|
+
const subscription = yield* redis.subscribe('domain-events');
|
|
239
|
+
|
|
240
|
+
yield* Stream.fromQueue(subscription).pipe(
|
|
241
|
+
Stream.runForEach(({ channel, message }) =>
|
|
242
|
+
handleRedisMessage(channel, message)
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}).pipe(Effect.scoped);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The subscription uses a dedicated client and is released when the enclosing scope closes. Node and Deno reconnect and re-subscribe after connection interruptions; messages published during recovery can be lost. Bun disables subscriber auto-reconnect: a dropped connection fails the dequeue with `RedisError`, and the caller must create a new scoped subscription. In every runtime, queue failure is observable by `Queue` and `Stream.fromQueue` consumers.
|
|
249
|
+
|
|
250
|
+
## Testing PubSub Services
|
|
251
|
+
|
|
252
|
+
Testing PubSub subscriptions requires specific choreography:
|
|
253
|
+
|
|
254
|
+
1. Fork the consumer fiber
|
|
255
|
+
2. Wait for subscriber readiness explicitly when possible; otherwise use a tiny registration barrier
|
|
256
|
+
3. Publish events
|
|
257
|
+
4. Gate on a `Deferred` for synchronization
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { Deferred, Effect, PubSub, Stream } from 'effect';
|
|
261
|
+
|
|
262
|
+
it.effect('should receive published events', () =>
|
|
263
|
+
Effect.gen(function* () {
|
|
264
|
+
const bus = yield* Bus.Service;
|
|
265
|
+
const received: Array<string> = [];
|
|
266
|
+
const done = yield* Deferred.make<void>();
|
|
267
|
+
|
|
268
|
+
// 1. Fork the consumer
|
|
269
|
+
yield* bus.subscribe(FileChanged).pipe(
|
|
270
|
+
Stream.runForEach((evt) =>
|
|
271
|
+
Effect.gen(function* () {
|
|
272
|
+
received.push(evt.path);
|
|
273
|
+
if (received.length === 2) {
|
|
274
|
+
yield* Deferred.succeed(done, undefined);
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
),
|
|
278
|
+
Effect.forkScoped
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// 2. Registration barrier
|
|
282
|
+
yield* Effect.sleep('10 millis');
|
|
283
|
+
|
|
284
|
+
// 3. Publish events
|
|
285
|
+
yield* bus.publish(new FileChanged({ path: 'a.ts', kind: 'modified' }));
|
|
286
|
+
yield* bus.publish(new FileChanged({ path: 'b.ts', kind: 'created' }));
|
|
287
|
+
|
|
288
|
+
// 4. Wait for events to be received
|
|
289
|
+
yield* Deferred.await(done);
|
|
290
|
+
|
|
291
|
+
expect(received).toEqual(['a.ts', 'b.ts']);
|
|
292
|
+
}).pipe(Effect.provide(Bus.layer))
|
|
293
|
+
);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Notes:**
|
|
297
|
+
|
|
298
|
+
- The `runForEach` handler is effectful, so complete the gate with `yield* Deferred.succeed(done, undefined)`. There is no `Deferred.unsafeDone` in v4; reach for the low-level `Deferred.doneUnsafe(done, Effect.void)` only inside a truly synchronous callback that has no surrounding effect.
|
|
299
|
+
- The tiny sleep above is an acceptable fallback for `Stream.fromPubSub` registration when no explicit readiness hook exists. If you control the consumer stream, prefer a readiness `Deferred` or latch instead.
|
|
300
|
+
- To drain a `PubSub` subscription for assertions, prefer `PubSub.takeUpTo(sub, n)`: it returns immediately with whatever is buffered (possibly an empty array). `PubSub.takeAll(sub)` **suspends when the subscription is empty** and returns a `NonEmptyArray`, so it cannot be used to assert “no more events” — it would hang waiting for one.
|
|
301
|
+
|
|
302
|
+
## PubSub Configuration
|
|
303
|
+
|
|
304
|
+
### Bounded vs Unbounded
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// Unbounded — no capacity limit / backpressure for active subscribers
|
|
308
|
+
const ps = yield* PubSub.unbounded<Event>();
|
|
309
|
+
|
|
310
|
+
// Bounded — applies backpressure when full
|
|
311
|
+
const ps = yield* PubSub.bounded<Event>(1024);
|
|
312
|
+
|
|
313
|
+
// Sliding — drops oldest events when full
|
|
314
|
+
const ps = yield* PubSub.sliding<Event>(1024);
|
|
315
|
+
|
|
316
|
+
// Dropping — drops newest events when full
|
|
317
|
+
const ps = yield* PubSub.dropping<Event>(1024);
|
|
318
|
+
|
|
319
|
+
// Optional replay buffer: late subscribers first receive the most recent N values
|
|
320
|
+
const withReplay = yield* PubSub.unbounded<Event>({ replay: 10 });
|
|
321
|
+
const boundedReplay = yield* PubSub.bounded<Event>({ capacity: 1024, replay: 10 });
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Choose based on your use case:
|
|
325
|
+
|
|
326
|
+
- **`unbounded`** — no capacity limit or backpressure; nothing is dropped for subscribers that are already attached
|
|
327
|
+
- **`bounded`** — when backpressure is acceptable and memory must be bounded
|
|
328
|
+
- **`sliding`** — when the latest events matter most (metrics, status updates)
|
|
329
|
+
- **`dropping`** — when burst absorption is needed but current events take priority
|
|
330
|
+
|
|
331
|
+
**PubSub is not an event log.** Messages are delivered to *active* subscribers only. A subscriber that attaches after a value was published does not see that value unless a `replay` buffer is configured, and `replay` only retains the most recent N values — it is bounded, recent-only, and not durable storage. If you need every consumer to observe the full history, subscribe before publishing (see the testing choreography above) or persist events separately.
|
|
332
|
+
|
|
333
|
+
## DO / DON'T
|
|
334
|
+
|
|
335
|
+
### DO: Use `Stream.fromPubSub` + `forkScoped` for subscriptions
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
yield*
|
|
339
|
+
Stream.fromPubSub(pubsub).pipe(
|
|
340
|
+
Stream.filter(isRelevant),
|
|
341
|
+
Stream.runForEach(handle),
|
|
342
|
+
Effect.forkScoped
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### DON'T: Use `PubSub.subscribe` with manual cleanup
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ❌ Overly complex — manual subscription management
|
|
350
|
+
const sub = yield* PubSub.subscribe(pubsub);
|
|
351
|
+
yield* Effect.acquireRelease(Effect.succeed(sub), (s) => Queue.shutdown(s));
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### DO: Shut down PubSub in finalizers
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub));
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### DON'T: Leave PubSub channels open
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// ❌ Resource leak — subscribers may hang indefinitely
|
|
364
|
+
const pubsub = yield* PubSub.unbounded<Event>();
|
|
365
|
+
// No shutdown registered
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### DO: Use `Stream.takeUntil` for shutdown-aware subscriptions
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
yield*
|
|
372
|
+
stream.pipe(
|
|
373
|
+
Stream.takeUntil((evt) => evt instanceof ShutdownEvent),
|
|
374
|
+
Stream.runForEach(handle),
|
|
375
|
+
Effect.forkScoped
|
|
376
|
+
);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Related Skills
|
|
380
|
+
|
|
381
|
+
- **effect-service-implementation**: Service declaration patterns
|
|
382
|
+
- **effect-layer-design**: Layer composition and dependency management
|
|
383
|
+
- **effect-stream**: Stream processing patterns
|
|
384
|
+
- **effect-testing**: Testing Effect programs with @effect/vitest
|