Effect-TS patterns for Streams. Use when working with streams in Effect-TS applications.
This skill provides 8 curated Effect-TS patterns for streams. Use this skill when working on tasks related to:
Rule: Use map and filter combinators to transform stream elements declaratively, creating pipelines that reshape data without materializing intermediate results.
Good Example:
This example demonstrates transforming a stream of raw data through multiple stages.
import { Stream, Effect, Chunk } from "effect";
interface RawLogEntry {
readonly timestamp: string;
readonly level: string;
readonly message: string;
}
interface ProcessedLog {
readonly date: Date;
readonly severity: "low" | "medium" | "high";
readonly normalizedMessage: string;
}
// Create a stream of raw log entries
const createLogStream = (): Stream.Stream<RawLogEntry> =>
Stream.fromIterable([
{ timestamp: "2025-12-17T09:00:00Z", level: "DEBUG", message: "App starting" },
{ timestamp: "2025-12-17T09:01:00Z", level: "INFO", message: "Connected to DB" },
{ timestamp: "2025-12-17T09:02:00Z", level: "ERROR", message: "Query timeout" },
{ timestamp: "2025-12-17T09:03:00Z", level: "DEBUG", message: "Retry initiated" },
{ timestamp: "2025-12-17T09:04:00Z", level: "WARN", message: "Connection degraded" },
{ timestamp: "2025-12-17T09:05:00Z", level: "INFO", message: "Recovered" },
]);
// Transform: Parse timestamp
const parseTimestamp = (entry: RawLogEntry): RawLogEntry => ({
...entry,
timestamp: entry.timestamp, // Already ISO, but could parse here
});
// Transform: Map log level to severity
const mapSeverity = (level: string): "low" | "medium" | "high" => {
if (level === "DEBUG" || level === "INFO") return "low";
if (level === "WARN") return "medium";
return "high";
};
// Transform: Normalize message
const normalizeMessage = (message: string): string =>
message.toLowerCase().trim();
// Filter: Keep only important logs
const isImportant = (entry: RawLogEntry): boolean => {
return entry.level !== "DEBUG";
};
// Main pipeline
const program = Effect.gen(function* () {
console.log(`\n[STREAM] Processing log stream with map/filter\n`);
// Create and transform stream
const transformedStream = createLogStream().pipe(
// Filter: Keep only non-debug logs
Stream.filter((entry) => {
const important = isImportant(entry);
console.log(
`[FILTER] ${entry.level} → ${important ? "✓ kept" : "✗ filtered out"}`
);
return important;
}),
// Map: Extract date
Stream.map((entry) => {
const date = new Date(entry.timestamp);
console.log(`[MAP-1] Parsed date: ${date.toISOString()}`);
return { ...entry, parsedDate: date };
}),
// Map: Normalize and map severity
Stream.map((entry) => {
const processed: ProcessedLog = {
date: entry.parsedDate,
severity: mapSeverity(entry.level),
normalizedMessage: normalizeMessage(entry.message),
};
console.log(
`[MAP-2] Transformed: ${entry.level} → ${processed.severity}`
);
return processed;
})
);
// Collect all transformed logs
const results = yield* transformedStream.pipe(
Stream.runCollect
);
console.log(`\n[RESULTS]`);
console.log(` Total logs: ${results.length}`);
Chunk.forEach(results, (log) => {
console.log(
` - [${log.severity.toUpperCase()}] ${log.date.toISOString()}: ${log.normalizedMessage}`
);
});
});
Effect.runPromise(program);
Output shows lazy evaluation and filtering:
[STREAM] Processing log stream with map/filter
[FILTER] DEBUG → ✗ filtered out
[FILTER] INFO → ✓ kept
[MAP-1] Parsed date: 2025-12-17T09:01:00.000Z
[MAP-2] Transformed: INFO → low
[FILTER] ERROR → ✓ kept
[MAP-1] Parsed date: 2025-12-17T09:02:00.000Z
[MAP-2] Transformed: ERROR → high
...
[RESULTS]
Total logs: 5
- [LOW] 2025-12-17T09:01:00.000Z: connected to db
- [HIGH] 2025-12-17T09:02:00.000Z: query timeout
...
Rationale:
Use Stream.map and Stream.filter to transform streams:
Pattern: stream.pipe(Stream.map(...), Stream.filter(...))
Streaming data transformations without map/filter create problems:
Map/filter enable:
Real-world example: Processing logs
logStream.pipe(Stream.filter(...), Stream.map(...))Rule: Use merge and concat combinators to combine multiple streams, enabling aggregation of data from multiple independent sources.
Good Example:
This example demonstrates merging multiple event streams into a unified stream.
import { Stream, Effect, Chunk } from "effect";
interface Event {
readonly source: string;
readonly type: string;
readonly data: string;
readonly timestamp: Date;
}
// Create independent event streams from different sources
const createUserEventStream = (): Stream.Stream<Event> =>
Stream.fromIterable([
{ source: "user-service", type: "login", data: "user-123", timestamp: new Date(Date.now() + 0) },
{ source: "user-service", type: "logout", data: "user-123", timestamp: new Date(Date.now() + 500) },
]).pipe(
Stream.tap(() => Effect.sleep("500 millis"))
);
const createPaymentEventStream = (): Stream.Stream<Event> =>
Stream.fromIterable([
{ source: "payment-service", type: "payment-started", data: "order-456", timestamp: new Date(Date.now() + 200) },
{ source: "payment-service", type: "payment-completed", data: "order-456", timestamp: new Date(Date.now() + 800) },
]).pipe(
Stream.tap(() => Effect.sleep("600 millis"))
);
const createAuditEventStream = (): Stream.Stream<Event> =>
Stream.fromIterable([
{ source: "audit-log", type: "access-granted", data: "resource-789", timestamp: new Date(Date.now() + 100) },
{ source: "audit-log", type: "access-revoked", data: "resource-789", timestamp: new Date(Date.now() + 900) },
]).pipe(
Stream.tap(() => Effect.sleep("800 millis"))
);
// Merge streams (interleaved, unordered)
const mergedEventStream = (): Stream.Stream<Event> => {
const userStream = createUserEventStream();
const paymentStream = createPaymentEventStream();
const auditStream = createAuditEventStream();
return Stream.merge(userStream, paymentStream, auditStream);
};
// Concat streams (sequential, ordered)
const concatenatedEventStream = (): Stream.Stream<Event> => {
return createUserEventStream().pipe(
Stream.concat(createPaymentEventStream()),
Stream.concat(createAuditEventStream())
);
};
// Main: Compare merge vs concat
const program = Effect.gen(function* () {
console.log(`\n[MERGE] Interleaved events from multiple sources:\n`);
// Collect merged stream
const mergedEvents = yield* mergedEventStream().pipe(
Stream.runCollect
);
Chunk.forEach(mergedEvents, (event, idx) => {
console.log(
` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`
);
});
console.log(`\n[CONCAT] Sequential events (user → payment → audit):\n`);
// Collect concatenated stream
const concatEvents = yield* concatenatedEventStream().pipe(
Stream.runCollect
);
Chunk.forEach(concatEvents, (event, idx) => {
console.log(
` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`
);
});
});
Effect.runPromise(program);
Output shows merge interleaving vs concat ordering:
[MERGE] Interleaved events from multiple sources:
1. [audit-log] access-granted: resource-789
2. [user-service] login: user-123
3. [payment-service] payment-started: order-456
4. [user-service] logout: user-123
5. [payment-service] payment-completed: order-456
6. [audit-log] access-revoked: resource-789
[CONCAT] Sequential events (user → payment → audit):
1. [user-service] login: user-123
2. [user-service] logout: user-123
3. [payment-service] payment-started: order-456
4. [payment-service] payment-completed: order-456
5. [audit-log] access-granted: resource-789
6. [audit-log] access-revoked: resource-789
Rationale:
Combine multiple streams using:
Pattern: Stream.merge(stream1, stream2) or stream1.pipe(Stream.concat(stream2))
Multi-source data processing without merge/concat creates issues:
Merge/concat enable:
Real-world example: Aggregating user events
Stream.merge(userStream, eventStream, notificationStream)Rule: Use backpressure control to manage flow between fast producers and slow consumers, preventing memory exhaustion and resource overflow.
Good Example:
This example demonstrates managing backpressure when consuming events at different rates.
import { Stream, Effect, Chunk } from "effect";
interface DataPoint {
readonly id: number;
readonly value: number;
}
// Fast producer: generates 100 items per second
const fastProducer = (): Stream.Stream<DataPoint> =>
Stream.fromIterable(Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.random() }))).pipe(
Stream.tap(() => Effect.sleep("10 millis")) // 10ms per item = 100/sec
);
// Slow consumer: processes 10 items per second
const slowConsumer = (item: DataPoint): Effect.Effect<void> =>
Effect.gen(function* () {
yield* Effect.sleep("100 millis"); // 100ms per item = 10/sec
});
// Without backpressure (DANGEROUS - queue grows unbounded)
const unbufferedStream = (): Stream.Stream<DataPoint> =>
fastProducer().pipe(
Stream.tap((item) =>
Effect.log(`[UNBUFFERED] Produced item ${item.id}`)
)
);
// With bounded buffer (backpressure kicks in)
const bufferedStream = (bufferSize: number): Stream.Stream<DataPoint> =>
fastProducer().pipe(
// Buffer at most 10 items; if full, producer waits
Stream.buffer(bufferSize),
Stream.tap((item) =>
Effect.log(`[BUFFERED] Consumed item ${item.id}`)
)
);
// With throttling (rate limit emission)
const throttledStream = (): Stream.Stream<DataPoint> =>
fastProducer().pipe(
// Emit at most 1 item per 50ms (20/sec)
Stream.throttle(1, "50 millis"),
Stream.tap((item) =>
Effect.log(`[THROTTLED] Item ${item.id}`)
)
);
// Main: compare approaches
const program = Effect.gen(function* () {
console.log(`\n[START] Demonstrating backpressure management\n`);
// Test buffered approach
console.log(`[TEST 1] Buffered stream (buffer size 5):\n`);
const startBuffer = Date.now();
yield* bufferedStream(5).pipe(
Stream.take(20), // Take only 20 items
Stream.runForEach(slowConsumer)
);
const bufferTime = Date.now() - startBuffer;
console.log(`\n[RESULT] Buffered approach took ${bufferTime}ms\n`);
// Test throttled approach
console.log(`[TEST 2] Throttled stream (1 item per 50ms):\n`);
const startThrottle = Date.now();
yield* throttledStream().pipe(
Stream.take(20),
Stream.runForEach(slowConsumer)
);
const throttleTime = Date.now() - startThrottle;
console.log(`\n[RESULT] Throttled approach took ${throttleTime}ms\n`);
// Summary
console.log(`[SUMMARY]`);
console.log(` Without backpressure control:`);
console.log(` - Queue would grow to 100 items (memory risk)`);
console.log(` - Producer/consumer operate independently`);
console.log(` With buffering:`);
console.log(` - Queue bounded to 5 items (safe)`);
console.log(` - Producer waits when buffer full`);
console.log(` With throttling:`);
console.log(` - Production rate limited to 20/sec`);
console.log(` - Smooth controlled flow`);
});
Effect.runPromise(program);
Rationale:
Backpressure is flow control: slow consumer tells fast producer to slow down.
Techniques:
Pattern: stream.pipe(Stream.throttle(...), Stream.buffer(...))
Without backpressure management, mismatched producer/consumer speeds cause:
Backpressure enable:
Real-world example: Reading large file vs. writing to database
Rule: Use scan for stateful element-by-element processing and fold for final aggregation, enabling complex stream analytics without buffering entire stream.
Good Example:
This example demonstrates maintaining statistics across a stream of measurements.
import { Stream, Effect, Chunk } from "effect";
interface Measurement {
readonly id: number;
readonly value: number;
readonly timestamp: Date;
}
interface RunningStats {
readonly count: number;
readonly sum: number;
readonly min: number;
readonly max: number;
readonly average: number;
readonly variance: number;
readonly lastValue: number;
}
// Create stream of measurements
const createMeasurementStream = (): Stream.Stream<Measurement> =>
Stream.fromIterable([
{ id: 1, value: 10, timestamp: new Date() },
{ id: 2, value: 20, timestamp: new Date() },
{ id: 3, value: 15, timestamp: new Date() },
{ id: 4, value: 25, timestamp: new Date() },
{ id: 5, value: 30, timestamp: new Date() },
{ id: 6, value: 22, timestamp: new Date() },
]);
// Initial statistics state
const initialStats: RunningStats = {
count: 0,
sum: 0,
min: Infinity,
max: -Infinity,
average: 0,
variance: 0,
lastValue: 0,
};
// Reducer: update stats for each measurement
const updateStats = (
stats: RunningStats,
measurement: Measurement
): RunningStats => {
const newCount = stats.count + 1;
const newSum = stats.sum + measurement.value;
const newAverage = newSum / newCount;
// Calculate variance incrementally
const delta = measurement.value - stats.average;
const delta2 = measurement.value - newAverage;
const newVariance = stats.variance + delta * delta2;
return {
count: newCount,
sum: newSum,
min: Math.min(stats.min, measurement.value),
max: Math.max(stats.max, measurement.value),
average: newAverage,
variance: newVariance / newCount,
lastValue: measurement.value,
};
};
// Main: demonstrate scan with statistics
const program = Effect.gen(function* () {
console.log(`\n[SCAN] Running statistics stream:\n`);
// Use scan to emit intermediate statistics
const statsStream = createMeasurementStream().pipe(
Stream.scan(initialStats, (stats, measurement) => {
const newStats = updateStats(stats, measurement);
console.log(
`[MEASUREMENT ${measurement.id}] Value: ${measurement.value}`
);
console.log(
` Count: ${newStats.count}, Avg: ${newStats.average.toFixed(2)}, ` +
`Min: ${newStats.min}, Max: ${newStats.max}, ` +
`Variance: ${newStats.variance.toFixed(2)}`
);
return newStats;
})
);
// Collect all intermediate stats
const allStats = yield* statsStream.pipe(Stream.runCollect);
// Final statistics
const finalStats = Chunk.last(allStats);
if (finalStats._tag === "Some") {
console.log(`\n[FINAL STATISTICS]`);
console.log(` Total measurements: ${finalStats.value.count}`);
console.log(` Average: ${finalStats.value.average.toFixed(2)}`);
console.log(` Min: ${finalStats.value.min}`);
console.log(` Max: ${finalStats.value.max}`);
console.log(
` Std Dev: ${Math.sqrt(finalStats.value.variance).toFixed(2)}`
);
}
// Compare with fold (emit only final result)
console.log(`\n[FOLD] Final statistics only:\n`);
const finalResult = yield* createMeasurementStream().pipe(
Stream.fold(initialStats, updateStats),
Stream.tap((stats) =>
Effect.log(`Final: Count=${stats.count}, Avg=${stats.average.toFixed(2)}`)
)
);
});
Effect.runPromise(program);
Rationale:
Stateful stream operations:
Pattern: stream.pipe(Stream.scan(initialState, reducer)) or Stream.fold(initialState, reducer)
Processing streams without scan/fold creates issues:
Scan/fold enable:
Real-world example: Running average of metrics
stream.pipe(Stream.scan(initialState, updateAverage))Rule: Use groupBy to partition streams by key and tumbling/sliding windows to aggregate streams over time windows.
Good Example:
This example demonstrates windowing and grouping patterns.
import { Effect, Stream, Ref, Duration, Schedule } from "effect";
interface Event {
readonly timestamp: Date;
readonly userId: string;
readonly action: string;
readonly duration: number; // milliseconds
}
// Simulate event stream
const generateEvents = (): Event[] => [
{ timestamp: new Date(Date.now() - 5000), userId: "user1", action: "click", duration: 100 },
{ timestamp: new Date(Date.now() - 4500), userId: "user2", action: "view", duration: 250 },
{ timestamp: new Date(Date.now() - 4000), userId: "user1", action: "scroll", duration: 150 },
{ timestamp: new Date(Date.now() - 3500), userId: "user3", action: "click", duration: 120 },
{ timestamp: new Date(Date.now() - 3000), userId: "user2", action: "click", duration: 180 },
{ timestamp: new Date(Date.now() - 2500), userId: "user1", action: "view", duration: 200 },
{ timestamp: new Date(Date.now() - 2000), userId: "user3", action: "view", duration: 300 },
{ timestamp: new Date(Date.now() - 1500), userId: "user1", action: "submit", duration: 500 },
{ timestamp: new Date(Date.now() - 1000), userId: "user2", action: "scroll", duration: 100 },
];
// Main: windowing and grouping examples
const program = Effect.gen(function* () {
console.log(`\n[WINDOWING & GROUPING] Stream organization patterns\n`);
const events = generateEvents();
// Example 1: Tumbling window (fixed-size batches)
console.log(`[1] Tumbling window (2-event batches):\n`);
const windowSize = 2;
let batchNumber = 1;
for (let i = 0; i < events.length; i += windowSize) {
const batch = events.slice(i, i + windowSize);
yield* Effect.log(`[WINDOW ${batchNumber}] (${batch.length} events)`);
let totalDuration = 0;
for (const event of batch) {
yield* Effect.log(
` - ${event.userId}: ${event.action} (${event.duration}ms)`
);
totalDuration += event.duration;
}
yield* Effect.log(`[WINDOW ${batchNumber}] Total duration: ${totalDuration}ms\n`);
batchNumber++;
}
// Example 2: Sliding window (overlapping)
console.log(`[2] Sliding window (last 3 events, slide by 1):\n`);
const windowSizeSlide = 3;
const slideBy = 1;
for (let i = 0; i <= events.length - windowSizeSlide; i += slideBy) {
const window = events.slice(i, i + windowSizeSlide);
const avgDuration =
window.reduce((sum, e) => sum + e.duration, 0) / window.length;
yield* Effect.log(
`[SLIDE ${i / slideBy}] ${window.length} events, avg duration: ${avgDuration.toFixed(0)}ms`
);
}
// Example 3: Group by key
console.log(`\n[3] Group by user:\n`);
const byUser = new Map<string, Event[]>();
for (const event of events) {
if (!byUser.has(event.userId)) {
byUser.set(event.userId, []);
}
byUser.get(event.userId)!.push(event);
}
for (const [userId, userEvents] of byUser) {
const totalActions = userEvents.length;
const totalTime = userEvents.reduce((sum, e) => sum + e.duration, 0);
const avgTime = totalTime / totalActions;
yield* Effect.log(
`[USER ${userId}] ${totalActions} actions, ${totalTime}ms total, ${avgTime.toFixed(0)}ms avg`
);
}
// Example 4: Group + Window combination
console.log(`\n[4] Group by user, window by action type:\n`);
for (const [userId, userEvents] of byUser) {
const byAction = new Map<string, Event[]>();
for (const event of userEvents) {
if (!byAction.has(event.action)) {
byAction.set(event.action, []);
}
byAction.get(event.action)!.push(event);
}
yield* Effect.log(`[USER ${userId}] Action breakdown:`);
for (const [action, actionEvents] of byAction) {
const count = actionEvents.length;
const total = actionEvents.reduce((sum, e) => sum + e.duration, 0);
yield* Effect.log(` ${action}: ${count}x (${total}ms total)`);
}
}
// Example 5: Session window (based on inactivity timeout)
console.log(`\n[5] Session window (gap > 1000ms = new session):\n`);
const sessionGapMs = 1000;
const sessions: Event[][] = [];
let currentSession: Event[] = [];
let lastTimestamp = events[0]?.timestamp.getTime() ?? 0;
for (const event of events) {
const currentTime = event.timestamp.getTime();
const timeSinceLastEvent = currentTime - lastTimestamp;
if (timeSinceLastEvent > sessionGapMs
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PaulJPhilp/effect-patterns-streams下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer