core/channel-effects
Use for consuming app-provided EventChannels in sagas, including websocket, DOM, IPC, and cleanup. For selector-derived channels, use core/selector-channels.
SKILL.mdskills/core/channel-effects/SKILL.md
GitHubChannel Effects — generic EventChannel consumers
Use native redux-saga
takeEvery(channel, worker)for every-eventEventChannel<T>consumption andtakeLatest(channel, worker)when each new event should cancel the previous worker.
Source: redux-saga channel watcher effects, Native channel watchers.
Imports
import type { EventChannel } from "redux-saga";
import { call, join, take, takeEvery, takeLatest } from "typed-redux-saga";
Native channel watchers
const watcher = yield* takeEvery(channel, worker); // or takeLatest
yield* join(watcher); // keep the resource owner's try block alive
Use typed effects with yield*; raw redux-saga/effects descriptors are not iterable. takeEvery and takeLatest fork an attached watcher and return its Task immediately. They do not own app-created channels. Keep the owner's try block alive with join(watcher) and close the channel in finally; otherwise it closes at startup, before later events arrive. Parent cancellation cancels attached workers and enters the owner's cleanup. Channel END finishes the watcher after attached workers finish.
Core Patterns
1. Fork a new worker for every event
// createAppEventChannel is app-provided and returns EventChannel<MyEvent>
const channel = createAppEventChannel<MyEvent>("my:event");
try {
const watcher = yield* takeEvery(channel, function* (data) {
yield* call(handleEvent, data);
});
yield* join(watcher);
} finally {
channel.close();
}
Every delivered event spawns a new worker concurrently — use when events are independent. Keep ownership of app-created channels explicit and close them in finally.
2. Cancel the previous worker for each new event
// createAppEventChannel is app-provided and returns EventChannel<MyEvent>
const channel = createAppEventChannel<MyEvent>("my:event");
try {
const watcher = yield* takeLatest(channel, function* (data) {
yield* call(expensiveOperation, data);
});
yield* join(watcher);
} finally {
channel.close();
}
Use native redux-saga takeLatest(channel, worker) when only the latest event matters (e.g., streaming progress, latest search). Keep ownership of app-created channels explicit and close them in finally.
3. Keep a serial while (true) + take(channel) loop when ordering matters
// Valid serial alternative: finish one operation before taking another event.
const channel = createAppEventChannel<MyEvent>("my:event");
try {
while (true) {
const data = yield* take(channel);
yield* call(handleEvent, data);
}
} finally {
channel.close();
}
This loop is not equivalent to takeEvery: it is serial rather than concurrent. Choose deliberately. An unbuffered eventChannel drops events while no taker is waiting (including while a serial worker runs). Configure the app's channel factory with an explicit buffer/overflow policy if those events must be retained; a bounded buffer also needs a deliberate overflow policy. Closing a channel unsubscribes; it does not by itself cancel already-forked workers.
Common Mistakes
❌ Omitting cleanup or closing immediately after forking a watcher
Mechanism: A loop without finally leaks its subscription on cancellation. A watcher fork without a blocking owner enters finally too early. Use the serial loop above or keep the forked watcher joined; the loop itself is not a mistake.
// WRONG
const ch = createAppEventChannel<Evt>("e");
while (true) {
const data = yield* take(ch);
yield* call(worker, data);
}
// CORRECT
const ch = createAppEventChannel<Evt>("e");
try {
const watcher = yield* takeEvery(ch, worker);
yield* join(watcher);
} finally {
ch.close();
}
Source: redux-saga channel watcher effects. Priority: HIGH.
❌ Using channel-effects where selector-channels fit
Mechanism: For selector-derived changes, takeEveryFromSelector / takeLatestFromSelector handle subscription and args-tuple keying automatically; wrapping a selector in a generic EventChannel throws those guarantees away.
// WRONG
const ch = yield* createChannelFromSelector(selectItem, id);
yield* takeLatest(ch, worker);
// CORRECT
yield* takeLatestFromSelector(selectItem, [id], worker);
Source context: package-internal store-utility saga implementation. Public selector-channel helpers are available from @themislib/themis/saga. Priority: MEDIUM.
See also
core/selector-channels— for selector-derived channels (the right tool for state changes).core/sagas— core saga patterns,takeEvery/takeLatestfor actions.