본문으로 건너뛰기
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.yaml530 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>

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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/skills252026년 10월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

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/skills252026년 10월 17일 업데이트

agents-inc의 스킬 모두 보기

이 스킬의 문제 신고