streaming/selectors
Author StreamingStore selectors returning Kefir Observable values, with observable arguments, Store binding, pure .select composition/tests, and saga .effect calls. Keep streaming selector internals p
SKILL.mdskills/streaming/selectors/SKILL.md
GitHubStreaming selectors — Kefir/observable call model
Use this skill when store.createSelector(...) belongs to a StreamingStore and
direct selector calls should produce Kefir Observable<R, any> results.
This Streaming selector model is exclusive to the StreamingStore family for the same app/package/code path. Do not apply alternate Store capture, template, or lifecycle/setup guidance here.
Authoring rules
- Create selectors through the configured
StreamingStoreinstance. - Keep this app on the Streaming Store family and keep alternate frontend patterns isolated to separate app/code paths.
- For pure derived callbacks and canonical state ownership, follow
../../core/state-integrity/SKILL.md— MUST / NEVER rules. - Compose selectors with
.select(state, ...args)inside another selector. - Keep generic selector helper modules Store-parameterized: accept a configured
store and call
store.createSelector(...)at the integration boundary. - Trust Store-owned selector-result and observable-output caching; do not add extra memoize/cache/debounce/throttle wrappers solely for performance.
- Prefer primitive scalar selector arguments over freshly constructed object, array, or function arguments; object/function args are valid only when their identity is stable and intentional.
- Do not import from any
themisstreaming-selector internal deep path; those are implementation internals, not package API.
import { StreamingStore } from "@themislib/themis/streaming-store";
export const streamStore = new StreamingStore({ todos: todosReducer });
export const selectTodoCount = streamStore.createSelector((state) => {
return state.todos.collection.ids.length;
});
This module defines selectors without reading stream state. Before invoking
them, follow ../selector-lifecycle/SKILL.md — Lifecycle map; for observation
and cleanup examples use Consumer subscription ownership in that same skill.
Call forms
| Context | Use | Result |
|---|---|---|
| Streaming consumer | selectFoo(...argsOrArgStreams) |
Kefir Observable<R, any> |
| Explicit StreamingStore binding | selectFoo.withStore(streamStore)(...args) |
Kefir Observable<R, any> |
| Tests/handlers/composition | selectFoo.select(state, ...args) |
Plain value R |
| Sagas | yield* selectFoo.effect(...args) |
typed-redux-saga select effect |
Selector arguments may be plain values or Kefir observables. Plain values are
lifted to constant streams before combining with the Store state stream.
The owning Store's throttledSelectorFrequency option (default 64 FPS)
cadences Store-state changes, not the combined selector output. Plain-argument
selectors have a prompt initial value; an observable argument must first emit
before the selector can emit. Later rapid Store writes coalesce to the latest
state at a scheduled tick. Observable-argument changes can recompute and emit
changed results immediately using the latest emitted Store state, even within
that interval. The option is not an overall output-rate cap. There is no separate
public fast-selector API; .select(store.state, ...plainArgs) and saga .effect
are uncadenced one-shot reads. For opt-in diagnostics and constructor
options, read ../../core/selector-tracing/SKILL.md — Scope and safety rules
and Configure the Store rather than adding a Streaming-specific trace policy.
Selector-channel helpers use the shared selector read shape, not direct Kefir
outputs. Their saga-context subscription behavior and plain stable argument
tuples are owned by ../../core/selector-channels/SKILL.md — Do and
Implementation cues; one-shot waits belong to ../../core/wait-for/SKILL.md
— Do. Saga read conventions belong to ../../core/sagas/SKILL.md — Do.
Selector caching
- Store-created selectors have internal selector-result caching/memoization.
- Direct Kefir
Observableoutputs are cached per StreamingStore instance + selector + arguments. Never-observed outputs and concurrent live consumers reuse the same observable. Removing one of several consumers retains it; removing the final observer evicts that output even if JavaScript references remain. The next identical call creates a new observable. Store disposal evicts all its outputs; cache counts are not active-observer counts. .withStore(streamStore)accepts an initialized StreamingStore object, not its raw Kefir stream. That Store is the cache source key and supplies state for computation. Two Store objects have separate cache entries even if they expose the same internal state stream.- Do not wrap selector callbacks or selector calls in extra
memoize,cache, manual cache maps, debounce, or throttle layers solely for performance. - Prefer the same Store-bound selector + same arguments over props drilling/manual stream passing when the receiving consumer can reasonably call the selector in valid streaming setup; otherwise use
.select,.effect, or.withStoreas the context requires.
Stable selector arguments
- Prefer primitive scalar selector arguments: ids, booleans, enum strings,
numbers,
null, orundefined. - Do not pass freshly constructed object, array, or function arguments to direct
selectFoo(...),.select(state, ...),.effect(...),.withStore(store)(...), selector-channel args tuples, orwaitForargs tuples. - Object/function args are valid only when the identity is stable and intentional, such as a module constant, memoized config, existing source object, or Kefir observable. Observable arguments are valid direct-call inputs when the stream is owned elsewhere; do not recreate a stream solely for a selector call.
- Prefer selector definitions like
(state, id, status)over(state, { id, status }); destructure an object arg only when callers pass a documented stable reference.
Collection reads
Use public collection utilities in selector callbacks when a package-level helper
is needed, and compose through .select(state) when another selector owns the
collection read.
import { getItem } from "@themislib/themis/utils/collections/collection-utils";
export const selectTodo = streamStore.createSelector((state, id: string) => {
return getItem(state.todos.collection, id);
});
Don't
- Do not teach alternate Store syntax or lifecycle setup for
StreamingStoreselectors. - Do not call another selector's direct streaming form inside a selector callback;
use
.select(state)to keep composition pure and synchronous. - Do not props-drill derived values solely to avoid selector calls when the consumer can call the same Store-bound selector with the same args in a valid streaming context.
- Do not add manual memoization/cache/debounce/throttle wrappers inside selector callbacks or around observable selector calls just to improve selector performance.
- Do not use standalone streaming selector utilities as public package imports.
- Do not construct
{ ... },[ ... ],() => ..., or new argument streams inline just to call a selector; pass scalar args or stable intentional references.
Verification cues
- Streaming selector examples return/observe Kefir observables only after the
StreamingStorehas been initialized. - Unit tests for pure selector logic use
.select(mockState, ...args). - Static scans show no public imports from
utils/streaming-selectors.
See also
../store/SKILL.md— Correct import and class choice.../selector-lifecycle/SKILL.md— Lifecycle map for invocation and teardown timing.../../core/state-integrity/SKILL.md— MUST / NEVER rules for derived-value ownership.