本文へ移動
cccskills
無料GitHub で公開

web-utilities-rxjs

Reactive programming with RxJS - Observables, operators, Subjects, error handling, and memory leak prevention

インストール方法を見る

含まれるファイル(8)

  • SKILL.md11.2 KB
  • examples/combination.md4.2 KB
  • examples/core.md4.6 KB
  • examples/higher-order.md4.0 KB
  • examples/memory-leaks.md4.1 KB
  • examples/subjects.md3.7 KB
  • metadata.yaml510 B
  • reference.md7.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

RxJS Reactive Programming Patterns

Quick Guide: RxJS composes async work as streams. Observables are lazy — nothing runs until subscribe() — and operators inside pipe() describe a pipeline rather than execute one. Three decisions carry most of the weight: which flattening operator (switchMap cancels, mergeMap parallelises, concatMap queues, exhaustMap ignores), where catchError sits (inside the inner pipe, or an error kills the outer stream permanently), and how the subscription ends. Suffix observable variables with $.

Detailed Resources:


Which path applies

Cleanup is the branch, and it is decided by whether the source ends on its own.

  • The source completes by itself — of, from, a wrapped Promise, timer(n), a single HTTP response. Nothing to unsubscribe; go straight to examples/core.md.
  • The source runs until stopped — fromEvent, interval, any Subject, a WebSocket. Every subscription needs an ending, and the shapes are in examples/memory-leaks.md.
  • You need one value out of a stream — firstValueFrom / lastValueFrom bridge to await and settle the lifetime for you. In examples/core.md.

<critical_requirements>

Before writing RxJS code

Give every long-lived subscription an ending — takeUntil with a destroy Subject, a Subscription collection, or a self-completing operator such as take or first. A fromEvent or interval subscription with no ending outlives whatever created it, holding its closure alive.

Put takeUntil last in the pipe. Operators only see what precedes them, so a takeUntil above a switchMap ends the outer source and leaves the inner Observable running — a leak that looks like correct cleanup code.

Put catchError inside the inner pipe, not the outer one. An error propagates to the subscriber and terminates the chain for good; recovering inside the inner Observable keeps the outer stream alive for the next value.

Pick the flattening operator from the concurrency you want — cancel previous (switchMap), run in parallel (mergeMap), queue (concatMap), ignore while busy (exhaustMap). Defaulting to mergeMap for a search box is how stale responses overwrite fresh ones.

</critical_requirements>


Auto-detection: RxJS, rxjs, Observable, Subject, BehaviorSubject, ReplaySubject, AsyncSubject, subscribe, pipe, switchMap, mergeMap, concatMap, exhaustMap, combineLatest, forkJoin, withLatestFrom, fromEvent, defer, interval, timer, catchError, retry, debounceTime, throttleTime, distinctUntilChanged, takeUntil, shareReplay, firstValueFrom, lastValueFrom, Subscription

Applies to:

  • Event streams that need composition — debounce, throttle, buffer, distinct
  • Coordinating several async sources — combine, fork-join, race, sequence
  • Cancellable work, where a new input should abandon the request in flight
  • Real-time data arriving over time — sockets, server-sent events, polling
  • Retry and backoff policies expressed as part of the pipeline

Handled elsewhere:

  • A single request with no stream semantics — async/await says it in one line, and wrapping it in an Observable buys nothing
  • Local component state — a stream is the wrong shape for a value that is simply read and written
  • Application state ownership — RxJS carries values between places; deciding where they live is a separate concern
  • Framework binding — converting a stream into whatever the view layer renders is that layer's own API

<philosophy>

Everything is a stream: clicks, responses, timers, socket frames. An Observable is a description of work rather than the work itself, so a pipe() chain builds a pipeline and subscribe() is what starts it — which is why a pipe with no subscriber does nothing at all, and why the same Observable subscribed twice usually does its work twice.

That laziness is also the source of the most common surprise: anything eager placed inside the chain (a fetch call evaluated at construction time rather than inside defer) escapes the model and runs once, shared, whether anyone subscribed or not.

</philosophy>
<patterns>

Core patterns

Pattern 1: Creation, and keeping it lazy

Match the creation function to the source, and wrap anything eager in defer so each subscriber gets its own execution.

import {
  of,
  from,
  fromEvent,
  interval,
  timer,
  defer,
  EMPTY,
  throwError,
} from "rxjs";

of(1, 2, 3); // fixed values, then completes
from([1, 2, 3]); // any iterable, then completes
fromEvent(element, "click"); // never completes
interval(1000); // never completes
timer(2000); // one value, then completes

defer(() => from(fetch("/api/users"))); // fresh request per subscriber
from(fetch("/api/users")); // ❌ fires at construction, shared by all

Full code: examples/core.md


Pattern 2: Operators in a pipe, never a nested subscribe

const adults$ = users$.pipe(
  filter((user) => user.age >= 18),
  map((user) => user.name),
  distinctUntilChanged(),
  tap((name) => console.log(name)), // side effects live in tap
);

A .subscribe() inside a .subscribe() creates a subscription nothing holds a reference to, so nothing can end it — the flattening operators exist to replace exactly this shape.

Full code: examples/core.md


Pattern 3: Error recovery with catchError

const results$ = query$.pipe(
  switchMap((query) =>
    defer(() => from(search(query))).pipe(
      retry({ count: 3, delay: (_, n) => timer(1000 * 2 ** (n - 1)) }),
      catchError(() => of({ results: [], failed: true })), // inner — outer survives
    ),
  ),
);

catchError must return an Observable, and where it sits decides what survives: inside the inner pipe the outer stream continues, outside it the whole chain ends on the first failure.

Full code: examples/core.md


Pattern 4: Rate limiting

// Wait for a pause — user input
input$.pipe(debounceTime(300), distinctUntilChanged());

// Cap the rate — continuous events
fromEvent(window, "scroll").pipe(throttleTime(200));

debounceTime emits the last value after silence; throttleTime emits the first value of each window and drops the rest. distinctUntilChanged after a debounce is what stops a type-then-undo from re-issuing the same request.

Full code: examples/higher-order.md, inside the search pipeline. Operator comparison: reference.md


Pattern 5: Flattening operators

The decision is what should happen when a new outer value arrives while the inner Observable is still running.

Cancel the previous inner and switch    → switchMap    (search, navigation)
Run the new one alongside               → mergeMap     (uploads, fire-and-forget)
Queue it until the previous finishes    → concatMap    (ordered saves, message send)
Ignore it until the current one ends    → exhaustMap   (submit, login, refresh)
query$.pipe(switchMap((q) => search(q)));
files$.pipe(mergeMap((file) => upload(file), 3)); // 3 = concurrency limit
saves$.pipe(concatMap((data) => save(data)));
clicks$.pipe(exhaustMap(() => submit()));

Full code: examples/higher-order.md


Pattern 6: Subjects

A Subject is both Observable and Observer, which makes it the way to multicast — and the way to push values in from imperative code.

new Subject<T>(); // no initial value, no replay
new BehaviorSubject<T>(initial); // always has a current value; .getValue()
new ReplaySubject<T>(n, windowMs); // replays the last n (optionally, recent ones)
new AsyncSubject<T>(); // the final value only, and only on complete()

BehaviorSubject for state that always has a value, ReplaySubject for history a late subscriber needs, Subject for events with no past, AsyncSubject for one final result.

Full code: examples/subjects.md


Pattern 7: Cleanup with takeUntil

class Controller {
  private readonly destroy$ = new Subject<void>();

  start(): void {
    source$
      .pipe(
        switchMap((id) => this.load(id)),
        takeUntil(this.destroy$), // last — ends the inner subscription too
      )
      .subscribe((data) => this.render(data));
  }

  stop(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

One destroy$ ends every subscription that pipes through it. Full code: examples/memory-leaks.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • An unhandled error terminates the chain permanently. Every subsequent emission is dropped silently, and the stream cannot be revived — only re-subscribed.
  • catchError returning a plain value rather than an Observable is a type error; of(fallback) is the fix.
  • takeUntil placed before a higher-order operator ends the outer source and leaves the inner subscription running. The cleanup code reads as correct and the leak is silent.
  • Nested .subscribe() calls produce subscriptions with no reference, multiplying with each outer emission.
  • firstValueFrom rejects when the Observable completes without emitting — the deprecated .toPromise() resolved undefined instead, so a migration changes the failure mode.

Surprising behaviour:

  • from(promise) runs the promise at construction, not at subscription, and every subscriber gets the same settled result. defer(() => from(promise)) restores laziness.
  • shareReplay(1) defaults refCount to false, so the source stays subscribed after the last subscriber leaves. shareReplay({ bufferSize: 1, refCount: true }) is almost always what was meant.
  • combineLatest emits nothing until every source has emitted at least once — one silent source stalls the whole combination.
  • forkJoin emits only when every source completes, so a Subject or an interval in the list means it never emits.
  • retry re-subscribes to the source from the top, so a non-idempotent source runs its side effects again.
  • interval(0) is still asynchronous — it schedules, it does not emit synchronously.
  • BehaviorSubject.getValue() reads the current value without subscribing, which quietly turns reactive code into a poll.
  • mergeMap where switchMap was meant produces a race: responses land out of order and the slowest one wins.

</red_flags>

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

日本語の概要は準備中です。原文の説明を表示しています。

agents-inc/skills242026年9月8日 更新

agents-inc のスキルをすべて見る

このスキルの問題を報告する