Mert Özen Her Satırda Daha İleri
Frontend RxJS — #13

RxJS 6'dan 7'ye ve 9'a: Değişen API'ler, Native Observable ve Angular Projesinde Ne Zaman Yükseltmeli?

Mert Özen 14 Kas 2026 18 dk 14 görüntülenme
RxJS 6'dan 7'ye ve 9'a: Değişen API'ler, Native Observable ve Angular Projesinde Ne Zaman Yükseltmeli?

Angular 22 projesi bugün RxJS 7.8'de kalmalı: Angular'ın peer aralığı ^6.5.3 || ^7.4.0 ve RxJS 9 beta'yla build, Angular'ın kendi rxjs/operators import'larında kırılıyor. RxJS 7'nin firstValueFrom, connectable ve share değişikliklerini, publishReplay taşımasındaki davranış farkını, RxJS 8'in neden çıkmadığını, RxJS 9'un Symbol operator'lerini, platform Observable'ını ve Chromium'daki native Observable'ı gerçek çalıştırmalarla inceliyoruz.

Sürüm notu: Bu yazıdaki örnekler RxJS 7.8.2 ve 9.0.0-beta.1, @rxjs/test ve @rxjs/migrate 9.0.0-beta.1, karşılaştırma için RxJS 6.6.7, TypeScript 6.0, Node.js 24, Vite 8.3 ve Angular 22.2 ile denenmiştir (Angular projesi, Angular 22.2'nin desteklediği TypeScript 6.0'ı kullanıyor); tarayıcı örnekleri Playwright 1.65 ile headless Chromium ve WebKit'te çalıştırılmıştır. npm sürüm bilgileri ve tarayıcı desteği 14 Kasım 2026'da kontrol edildi.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) eski bir modülünde, RxJS 6 döneminden kalma bir kod bir kitabın gün içindeki stok değişikliklerini bir Promise'e çeviriyor. Bugün stok hiç değişmemiş, yani akış hiç değer vermeden tamamlanıyor. Aynı akışı RxJS 6 ve RxJS 7 yöntemleriyle bekleyelim:

import { EMPTY, firstValueFrom, lastValueFrom, of } from "rxjs";

// bugün hiç değişmeyen bir kitabın stok değişiklikleri: akış değer vermeden tamamlanır
const todaysChanges$ = EMPTY;

// RxJS 6 yöntemi: RxJS 7'den beri deprecated
const legacy = await todaysChanges$.toPromise();
console.log("toPromise:", legacy);

// RxJS 7 yöntemi
try {
  const last = await lastValueFrom(todaysChanges$);
  console.log("lastValueFrom:", last);
} catch (err) {
  console.log("lastValueFrom failed:", (err as Error).name, "-", (err as Error).message);
}

console.log("with defaultValue:", await lastValueFrom(todaysChanges$, { defaultValue: 0 }));
console.log("firstValueFrom(of(12, 11, 9)):", await firstValueFrom(of(12, 11, 9)));
console.log("lastValueFrom(of(12, 11, 9)):", await lastValueFrom(of(12, 11, 9)));

toPromise() ne döndürür? lastValueFrom aynı akışta ne yapar?

Seri boyunca RxJS 7.8 kullandık. Bu son yazıda RxJS'in nereden gelip nereye gittiğine bakıyoruz: RxJS 6'dan 7'ye değişen API'ler ve deprecated publish ailesinin taşınması, hiç yayımlanmayan RxJS 8, Ağustos 2026'da beta'ya çıkan RxJS 9 ve tarayıcıya gelen native Observable. Sonunda da asıl soruya cevap veriyoruz: bir Angular 22 projesi ne zaman yükseltmeli?

RxJS 7'nin Getirdikleri

Çalıştırınca:

toPromise: undefined
lastValueFrom failed: EmptyError - no elements in sequence
with defaultValue: 0
firstValueFrom(of(12, 11, 9)): 12
lastValueFrom(of(12, 11, 9)): 9

toPromise() boş akışta sessizce undefined ile çözüldü; çağıran kod "stok 0" mı yoksa "değer yok" mu olduğunu ayırt edemiyor. RxJS 7 bu yüzden toPromise'i deprecated yaptı, dönüş tipini Promise<T | undefined> olarak düzeltti ve yerine iki fonksiyon getirdi. firstValueFrom ilk değeri alıp subscription'ı kapatıyor; lastValueFrom tamamlanmayı bekleyip son değeri veriyor. İkisi de değer gelmezse EmptyError ile reject oluyor; boş durumun anlamlı bir karşılığı varsa defaultValue veriyorsun. 7.8.2'nin type tanımları toPromise'in v8'de kaldırılacağını yazıyor.

RxJS 7'nin (Nisan 2021) diğer değişikliklerini bu seride zaten kullandık:

RxJS 6RxJS 7Bu seride
toPromise()firstValueFrom, lastValueFromBu yazı
retryWhenretry({ count, delay, resetOnSuccess }) (7.3'ten beri)Error handling yazısı
timeout(ms) + timeoutWithtimeout({ first, each, with })Zaman operator'leri yazısı
share() (ayarsız)share({ connector, resetOnError, resetOnComplete, resetOnRefCountZero })Multicasting yazısı
publish, multicast, refCountconnectable, connect, shareBu yazı
import { map } from 'rxjs/operators'import { map } from 'rxjs' (7.2'den beri)Bütün örnekler

RxJS 7'nin changelog'u ayrıca operator implementasyonlarında büyük bir boyut azalmasından söz ediyor. Bunu kendi arama pipeline'ımızla ölçtük: aynı kodu (debounceTime, map, distinctUntilChanged, switchMap, catchError) RxJS 6.6.7, 7.8.2 ve 9.0.0-beta.1 ile Vite'ta minify ederek build ettik. Angular CLI gibi RxJS 6 ve 7'nin ES5 değil ES2015 build'lerini seçtik:

v6:  17.1 kB minified,  5.0 kB gzip
v7:  15.7 kB minified,  5.1 kB gzip
v9:  17.3 kB minified,  5.1 kB gzip

Bu küçük pipeline için fark gzip'te 0,1 kB. Boyut kazancı çok operator kullanan büyük bir uygulamada daha görünür olabilir, ama tek başına bir yükseltme sebebi değil. Ölçerken bir tuzağa da düştük: Vite'ın varsayılan ayarlarıyla RxJS 7'nin ES5 build'i seçildi ve bundle 15,7 yerine 21,5 kB (gzip'te 5,1 yerine 6,5 kB) çıktı. Angular CLI bu yüzden es2015 condition'ını kendisi ekliyor.

6'dan 7'ye taşınırken custom operator yazısında kurduğumuz eslint-plugin-rxjs-x işe yarıyor: recommended config'teki no-topromise kalan toPromise çağrılarını, prefer-root-operators da rxjs/operators import'larını buluyor.

publish ve refCount'tan share ve connectable'a

Multicasting yazısında RxJS 7'nin publish, publishReplay, multicast ve refCount'u deprecated yaptığını söylemiş, taşımayı bu yazıya bırakmıştık. En yaygın kalıp, bir HTTP isteğini publishReplay(1) ve refCount() ile paylaşmak. İlk akla gelen karşılığı shareReplay({ bufferSize: 1, refCount: true }):

// kategori listesi: tek istek, 200 ms sonra cevap
const categories$ = defer(() => {
  log("-> GET /api/categories");
  return timer(200).pipe(map(() => "classics, fantasy"));
});

function showSharing(shared: Observable<string>) {
  shared.subscribe((list) => log(`nav: ${list}`));
  shared.subscribe((list) => log(`select: ${list}`));
  timer(500).subscribe(() => shared.subscribe((list) => log(`late: ${list}`)));
}

runInVirtualTime("RxJS 6: publishReplay(1) + refCount()", () => {
  showSharing(categories$.pipe(publishReplay(1), refCount()));
});

runInVirtualTime("RxJS 7: shareReplay({ bufferSize: 1, refCount: true })", () => {
  showSharing(categories$.pipe(shareReplay({ bufferSize: 1, refCount: true })));
});
--- RxJS 6: publishReplay(1) + refCount()
    0 ms  -> GET /api/categories
  200 ms  nav: classics, fantasy
  200 ms  select: classics, fantasy
  500 ms  late: classics, fantasy
--- RxJS 7: shareReplay({ bufferSize: 1, refCount: true })
    0 ms  -> GET /api/categories
  200 ms  nav: classics, fantasy
  200 ms  select: classics, fantasy
  500 ms  late: classics, fantasy

Her şey yolunda giderken çıktılar aynı: tek istek, iki subscriber ve 500. ms'de gelen geç subscriber cache'teki değeri alıyor. İlk istek 503 alırsa durum değişiyor:

runInVirtualTime("after an error, RxJS 6: publishReplay(1) + refCount()", () => {
  showRecovery(flakyCategories$.pipe(publishReplay(1), refCount()));
});

runInVirtualTime("after an error, RxJS 7: shareReplay({ bufferSize: 1, refCount: true })", () => {
  showRecovery(flakyCategories$.pipe(shareReplay({ bufferSize: 1, refCount: true })));
});

// RxJS deprecation rehberindeki birebir karşılık: hiç reset yok, hata da saklanır
runInVirtualTime("after an error, RxJS 7: share() with a ReplaySubject, no resets", () => {
  showRecovery(
    flakyCategories$.pipe(
      share({
        connector: () => new ReplaySubject(1),
        resetOnError: false,
        resetOnComplete: false,
        resetOnRefCountZero: false,
      }),
    ),
  );
});
--- after an error, RxJS 6: publishReplay(1) + refCount()
    0 ms  -> GET /api/categories
  200 ms  nav failed: HTTP 503
  500 ms  late failed: HTTP 503
--- after an error, RxJS 7: shareReplay({ bufferSize: 1, refCount: true })
    0 ms  -> GET /api/categories
  200 ms  nav failed: HTTP 503
  500 ms  -> GET /api/categories
  700 ms  late: classics, fantasy
--- after an error, RxJS 7: share() with a ReplaySubject, no resets
    0 ms  -> GET /api/categories
  200 ms  nav failed: HTTP 503
  500 ms  late failed: HTTP 503

publishReplay tek bir ReplaySubject kullanıyor ve hata da o Subject'te kalıyor: geç gelen subscriber yeni bir istek göndermeden aynı hatayı alıyor. shareReplay ise hatada sıfırlanıyor ve geç subscriber için isteği yeniden gönderiyor. Bu örnekte yeni davranış daha iyi, ama yine de bir davranış değişikliği; birebir karşılığı, RxJS'in deprecation rehberinin de önerdiği üç reset seçeneği false olan share. Mekanik bir bul-değiştir ile taşınan kod, hata durumunda farklı davranabilir; multicasting yazısında hatalı yanıtı cache'lemenin neden sorun olduğunu görmüştük. Taşırken hangisini istediğine karar ver ve o davranışı test et.

publish() ile elde edilen ConnectableObservable'ın karşılığı connectable(). İkisini aynı polling akışıyla çalıştırdık ve dokuz satırlık çıktı birebir aynı çıktı:

runInVirtualTime("RxJS 6: publish() + connect()", () => {
  const shared = stock$.pipe(publish()) as ConnectableObservable<number>;
  shared.subscribe((i) => log(`screen: poll ${i + 1}`));
  shared.subscribe((i) => log(`badge: poll ${i + 1}`));
  shared.connect();
});

runInVirtualTime("RxJS 7: connectable() + connect()", () => {
  const shared = connectable(stock$);
  shared.subscribe((i) => log(`screen: poll ${i + 1}`));
  shared.subscribe((i) => log(`badge: poll ${i + 1}`));
  shared.connect();
});
--- RxJS 7: connectable() + connect()
 1000 ms  -> GET /api/books/9780441172719/stock (poll 1)
 1000 ms  screen: poll 1
 1000 ms  badge: poll 1
 2000 ms  -> GET /api/books/9780441172719/stock (poll 2)
 2000 ms  screen: poll 2
 2000 ms  badge: poll 2
 3000 ms  -> GET /api/books/9780441172719/stock (poll 3)
 3000 ms  screen: poll 3
 3000 ms  badge: poll 3

RxJS 8 Neden Çıkmadı, RxJS 9 Ne Getiriyor?

npm'deki sürüm geçmişi hikâyeyi özetliyor: 8.0.0-alpha.0 Ocak 2022'de, son alpha olan 8.0.0-alpha.14 Ocak 2024'te yayımlandı; kararlı bir RxJS 8 hiç çıkmadı. Bu sürede 7.x bakımda kaldı ve latest hâlâ Şubat 2025 tarihli 7.8.2. RxJS 9'un README'si sebebi de söylüyor: RxJS 8 çalışması, Web Platform'un Observable'ı netleşene kadar beklemeye alındı. RxJS 9 o bekleyen kodun devamı değil, platform Observable'ı üzerine kurulmuş yeni bir nesil; sürüm numarası da eski RxJS 8 çalışmasıyla karıştırılmaması için 9. İlk beta 9.0.0-beta.0 4 Ağustos 2026'da, 9.0.0-beta.1 9 Ekim 2026'da yayımlandı ve npm'de next tag'iyle duruyor.

Değişiklik bir sürüm atlamasından büyük. RxJS 9 kendi Observable class'ını getirmiyor: ortamda native bir Observable varsa onu kullanıyor, yoksa ayrı yayımlanan @rxjs/observable-polyfill'i kuruyor. Operator'ler .pipe()'a verilen fonksiyonlar değil, Observable'a Symbol anahtarıyla eklenen method'lar ve her biri kendi subpath'inden import ediliyor. Bu bölümdeki örnekler snapshot'taki ayrı rxjs9-trial projesinde; npm ci'dan sonra node src/01-symbols.ts ile çalışıyor, testler npm test, bundle ölçümü npm run bundles ile:

// Node 24'te native Observable yok: rxjs'i import etmek @rxjs/observable-polyfill'i kurar
import "./before-rxjs.ts";
import { ColdObservable } from "rxjs";
import { map } from "rxjs/map";
import { scan } from "rxjs/scan";

console.log("Observable after rxjs:", typeof Observable);

type CartItem = { title: string; price: number; quantity: number };

// sepet: 2 x Dune, 1 x Emma, 1 x The Hobbit
const cart = new ColdObservable<CartItem>((subscriber) => {
  subscriber.next({ title: "Dune", price: 450, quantity: 2 });
  subscriber.next({ title: "Emma", price: 320, quantity: 1 });
  subscriber.next({ title: "The Hobbit", price: 390, quantity: 1 });
  subscriber.complete();
});

// operator'ler Symbol: source.pipe(map(…)) yerine source[map](…)
const result = cart[map]((item) => item.price * item.quantity)
  [scan]((total, line) => total + line, 0)
  .subscribe({
    next: (total) => console.log(`subtotal: ${total} TRY`),
    complete: () => console.log("done"),
  });

console.log("subscribe() returns:", result);
console.log("cart.pipe:", (cart as unknown as { pipe?: unknown }).pipe);
Observable before rxjs: undefined
Observable after rxjs: function
subtotal: 900 TRY
subtotal: 1220 TRY
subtotal: 1610 TRY
done
subscribe() returns: undefined
cart.pipe: undefined

Node.js 24'te native Observable yok; rxjs'i import etmek global Observable'ı polyfill ile kurdu. map ve scan birer unique symbol; cart[map](…) bracket syntax'ıyla çağrılıyor ve root import operator kataloğunu kurmuyor, yalnız import ettiğin Symbol'ler var. subscribe() artık bir Subscription döndürmüyor, undefined döndürüyor; .pipe yok. Paketin migration rehberine göre OperatorFunction tip ailesi, rxjs/operators, scheduler'lar (asyncScheduler ve diğerleri) ve TestScheduler da yok. Paket yalnız ESM (ECMAScript modülleri; import/export ile yüklenen modül biçimi) olarak yayımlanıyor ve Node 22.13 ya da üstünü istiyor; require('rxjs') Node'un require(esm) desteğiyle çalışıyor. Bu serideki custom operator'lerin karşılığı da değişiyor: RxJS 9'da bir operator'ü sarmak için sıradan, tipli bir fonksiyon yazıyorsun (aşağıdaki settledTerm gibi).

Platform Observable ve ColdObservable

Bu serinin ilk yazısında "her subscribe producer'ı yeniden çalıştırır" demiştik; HttpClient'ın iki async pipe ile iki istek göndermesi de bundan geliyordu. Platform Observable'ın sözleşmesi farklı: aynı anda subscribe olanlar tek producer'ı paylaşıyor, son subscriber ayrılınca producer kapanıyor, sonra gelen yeniden başlatıyor. Eski davranışı isteyen için RxJS 9 ColdObservable'ı veriyor. Aynı sahte isteği ikisiyle de çalıştırdık; iptal artık AbortController ile:

import { ColdObservable } from "rxjs";

// sahte bir GET /api/categories: istek başlayınca ve teardown çalışınca log yazar
function categoriesProducer(subscriber: Subscriber<string>) {
  console.log("-> GET /api/categories");
  const handle = setTimeout(() => {
    subscriber.next("classics, fantasy");
    subscriber.complete();
  }, 200);
  subscriber.addTeardown(() => {
    clearTimeout(handle);
    console.log("   teardown");
  });
}

async function twoSubscribers(title: string, source: Observable<string>) {
  console.log(`--- ${title}`);
  const nav = new AbortController();
  const select = new AbortController();
  source.subscribe((list) => console.log(`nav: ${list}`), { signal: nav.signal });
  source.subscribe((list) => console.log(`select: ${list}`), { signal: select.signal });
  await new Promise((resolve) => setTimeout(resolve, 300));
}

await twoSubscribers("platform Observable", new Observable(categoriesProducer));
await twoSubscribers("ColdObservable", new ColdObservable(categoriesProducer));

console.log("--- abort before the answer");
const source = new Observable(categoriesProducer);
const nav = new AbortController();
const select = new AbortController();
source.subscribe((list) => console.log(`nav: ${list}`), { signal: nav.signal });
source.subscribe((list) => console.log(`select: ${list}`), { signal: select.signal });
nav.abort();
console.log("nav aborted");
select.abort();
console.log("select aborted");
--- platform Observable
-> GET /api/categories
nav: classics, fantasy
select: classics, fantasy
   teardown
--- ColdObservable
-> GET /api/categories
-> GET /api/categories
nav: classics, fantasy
   teardown
select: classics, fantasy
   teardown
--- abort before the answer
-> GET /api/categories
nav aborted
   teardown
select aborted

Platform Observable ile iki subscriber tek istek gördü; ColdObservable ile iki istek gitti. Son bölümde istek sürerken önce nav ayrıldı ve hiçbir şey olmadı, select da ayrılınca teardown çalıştı. Teardown'u artık fonksiyondan döndürmüyor, subscriber.addTeardown() ile kaydediyorsun. Bu fark, RxJS 7'den taşınan her akış için bir karar demek: multicasting yazısındaki share'in yaptığı işin bir kısmını platform kendisi yapıyor, ama tam olarak aynısını değil. Migration rehberi de her pipeline için hedef yaşam döngüsünü (platform, ColdObservable ya da Subject) önce seçmeyi istiyor.

Tarayıcıda Native Observable

Observable, WICG'de (Web Incubator Community Group; tarayıcı API'lerinin standartlaşmadan önce tartışıldığı topluluk grubu) ilerleyen bir öneri olarak tarayıcıya geldi. Ana giriş noktası EventTarget.when(): her DOM (tarayıcının sayfadan kurduğu element ağacı) event'i bir Observable'a dönüşüyor ve map, filter, takeUntil, switchMap, first gibi bir grup operator platformun kendi method'u olarak geliyor. MDN'nin (Mozilla'nın web geliştirici dokümantasyonu) browser-compat-data paketinin 8.1.9 sürümüne (12 Kasım 2026) göre Observable ve EventTarget.when Chrome ve Edge'de 135'ten beri var, Firefox ve Safari'de yok; ikisi de experimental olarak işaretli. Arama kutusunu platformun when ve map'i ile, RxJS 9'un Symbol operator'leriyle birlikte yazdık:

// tarayıcının Observable'ı üzerinde RxJS 9: EventTarget.when() + RxJS Symbol operator'leri
import { getObservablePolyfillInfo } from "@rxjs/observable-polyfill";
import "rxjs";
import { debounce } from "rxjs/debounce";
import { distinctUntilChanged } from "rxjs/distinct-until-changed";
import { map } from "rxjs/map";

console.log(`Observable: ${getObservablePolyfillInfo() ? "RxJS fallback" : "native"}`);

const box = document.querySelector<HTMLInputElement>("#search")!;
box
  .when("input")
  .map(() => box.value) // platformun kendi map'i
  [debounce](300) // Symbol'ler üzerinden RxJS operator'leri
  [map]((term) => term.trim())
  [distinctUntilChanged]()
  .subscribe((term) => console.log(`search: ${term}`));

Playwright her iki motorda "dune" yazdı, yarım saniye sonra sona bir boşluk ekledi, sonra kutuya "emma" yazdı:

--- Chromium 158.0.8127.18: Observable function, EventTarget.when function
   10 ms  Observable: native
  638 ms  search: dune
 1765 ms  search: emma
--- WebKit 27.2: Observable undefined, EventTarget.when undefined
    7 ms  Observable: RxJS fallback
  640 ms  search: dune
 1767 ms  search: emma

Chromium'da RxJS 9 native Observable'ı kullandı; Playwright'ın WebKit build'inde ikisi de yoktu ve polyfill devreye girdi. Davranış iki motorda da aynı: boşluk trim'den sonra aynı terimi verdi ve atlandı. Aynı zincirde .map(…) platformun, [map](…) RxJS'in method'u; RxJS'in import'u platform method'unu ezmiyor. Firefox'u bu ortamda çalıştıramadık; Firefox için bilgi yalnız browser-compat-data'dan geliyor.

RxJS 9'da Test ve Migration: @rxjs/test ve @rxjs/migrate

Bir önceki yazının marble testleri TestScheduler'a dayanıyordu ve RxJS 9'da TestScheduler yok; yerini ayrı bir paket olan @rxjs/test'in rxTest'i alıyor. settledTerm'i RxJS 9'da sıradan bir fonksiyon olarak yazıp aynı marble'larla test ettik:

import "rxjs";
import { debounce } from "rxjs/debounce";
import { distinctUntilChanged } from "rxjs/distinct-until-changed";
import { map } from "rxjs/map";

// RxJS 9'da OperatorFunction yok: custom operator sıradan bir fonksiyon
export function settledTerm(terms: Observable<string>, dueTimeMs = 300): Observable<string> {
  return terms[debounce](dueTimeMs)[map]((term) => term.trim())[distinctUntilChanged]();
}
import { rxTest } from "@rxjs/test";
import { expect, it } from "vitest";
import { settledTerm } from "./settled-term.ts";

it("emits a term 300 ms after the last key", async () => {
  await rxTest(({ hot, expectObservable }) => {
    const input = hot("a 99ms b 99ms c 99ms d", { a: "d", b: "du", c: "dun", d: "dune" });
    // rxTest sonda hâlâ açık olan akışı kabul etmez: 1000. ms'de unsubscribe
    expectObservable(settledTerm(input, 300), "^ 999ms !").toBe("600ms d", { d: "dune" });
  });
});

it("trims the term and skips a repeat", async () => {
  await rxTest(({ hot, expectObservable }) => {
    const input = hot("a 399ms b 399ms c", { a: "dune", b: "dune ", c: " emma" });
    expectObservable(settledTerm(input, 300), "^ 1999ms !").toBe("300ms a 799ms c", {
      a: "dune",
      c: "emma",
    });
  });
});

Marble'lar RxJS 7 sürümüyle aynı ve testler geçiyor. Tek fark ilk denemede çıktı: unsubscription marble'ı ("^ 999ms !") olmadan iki test de şu hatayla düştü:

 FAIL  src/settled-term.test.ts > emits a term 300 ms after the last key
Error: rxTest finished with 1 open observation(s). Complete the source or provide an unsubscription marble.

TestScheduler tamamlanmayan bir akışı sessizce kabul ediyordu; rxTest, test bittiğinde hâlâ açık bir subscription varsa ne zaman bırakılacağını açıkça yazmanı istiyor. Bir metin kutusu hiç tamamlanmadığı için bu, testin gerçek dünyayı daha dürüst anlatması demek.

@rxjs/migrate RxJS 9 migration'ı için deterministik bir dönüşüm motoru ve Claude Code, Codex ve Cursor için kurulabilen bir "Skill" ile geliyor. README'sine göre mekanik olarak desteklenen küme şimdilik küçük: filter, map, takeUntil, debounceTime, concatMap ve birkaç operator daha içeren düz pipe(…) zincirleri. Önce bir dry run çalıştırdık; araç dosyaya hiçbir şey yazmadan sonucu JSON olarak veriyor. Basit bir arama fonksiyonu için çıktısı:

// yazıdaki @rxjs/migrate dry run'ı için RxJS 7 girdisi (bu projede derlenmez)
import { debounceTime, filter, map, type Observable } from "rxjs";

// arama kutusu: duraklamayı bekle, kırp, boş terimleri yok say
export function searchTerms(input: Observable<string>): Observable<string> {
  return input.pipe(
    debounceTime(300),
    map((term) => term.trim()),
    filter((term) => term.length > 0),
  );
}
// Migrated from https://github.com/example/bookstore @ abc123
// Source: src/search-terms.ts
import { debounce } from "rxjs/debounce";
import { map } from "rxjs/map";
import { filter } from "rxjs/filter";
import { type Observable } from "rxjs";
// the search box: wait for a pause, trim, ignore empty terms
export function searchTerms(input: Observable<string>): Observable<string> {
    return input[debounce](300)[map]((term) => term.trim())[filter]((term) => term.length > 0);
}

debounceTime(300), RxJS 9'da [debounce](300) oldu; RxJS 9'da ayrı bir debounceTime yok, debounce sayı ya da fonksiyon alıyor. Ama çıktı RxJS 9 projesinde olduğu gibi derlenmedi: tsc, Module '"rxjs"' has no exported member 'Observable' dedi, çünkü RxJS 9'da Observable tipi global ve root'tan export edilmiyor. O import satırını silince derlendi. Aynı aracı önceki yazının 02-settled-term.test.ts'i üzerinde çalıştırınca ise dönüşümü reddetti:

"status": "refused"
"code": "lifecycle-review"
"message": "Multiple observations may depend on producer multiplicity."
"nextAction": "Verify cold versus shared platform behavior before accepting the migration."
"span": src/12/02-settled-term.test.ts line 23

Araç, aynı hot kaynağına birden fazla gözlemin bağlandığı yerde cold ile paylaşılan platform davranışı arasındaki farkın sonucu değiştirebileceğini söylüyor ve kararı insana bırakıyor. Bu doğru bir tasarım: RxJS 9'a geçişin zor kısmı söz dizimi değil, her akışın yaşam döngüsü kararı. Bir not: bizim denememizde npx rxjs-migrate komutu hiçbir şey yazmadan 0 ile çıktı; CLI dosyasını (node node_modules/@rxjs/migrate/dist/esm/cli.js …) doğrudan çalıştırınca çalıştı. Beta bir araçtan beklenecek türden bir sorun.

Karar: Angular Projesi Ne Zaman Yükseltmeli?

Angular'ın bu konudaki tavrı package.json'da yazıyor. @angular/core 22.2.6 (latest) ve 22.3.0-rc.0 (next) için peer bağımlılık aynı: rxjs: "^6.5.3 || ^7.4.0". Yine de denedik: bookstore-web'in bir kopyasına RxJS 9 beta'yı kurup build aldık:

$ npm i rxjs@9.0.0-beta.1
npm warn ERESOLVE overriding peer dependency
npm warn Could not resolve dependency:
npm warn peer rxjs@"^6.5.3 || ^7.4.0" from @angular/common@22.2.6
...
$ npx ng build
✘ [ERROR] Could not resolve "rxjs/operators"

    node_modules/@angular/common/fesm2022/http.mjs:11:20:
      11 │ import { tap } from 'rxjs/operators';
         ╵                     ~~~~~~~~~~~~~~~~

  The path "./operators" is not exported by package "rxjs":
...
✘ [ERROR] TS2305: Module '"rxjs"' has no exported member 'of'.

    src/app/core/operators.ts:8:2:
      8 │   of,
...
$ grep -c ERROR build.log
73

npm kurulumu durdurmadı; peer uyuşmazlığını uyarıyla geçti. Build ise 73 hatayla düştü ve ilk hatalar bizim kodumuzdan değil, Angular'ın kendisinden geliyor: @angular/common, core, forms ve router, RxJS 9'da olmayan rxjs/operators'tan import ediyor. Bu yüzden sorunun cevabı bugün net:

  • Production bir Angular 22 projesi RxJS 7.8'de kalır. 7.x bakımda ve latest hâlâ o.
  • RxJS 6 kullanan bir proje 7.8'e geçmeli; bu yazıdaki tablo, publish ailesinin karşılıkları ve eslint-plugin-rxjs-x'in kuralları bu geçişin listesi.
  • Bugünden yapılabilecek hazırlık RxJS 7 içinde: deprecated API'leri (toPromise, publish ailesi, rxjs/operators) temizlemek, custom operator'leri az sayıda dosyada toplamak ve davranışı (paylaşım, iptal, hata) testlerle sabitlemek. RxJS 9'un migration rehberinin ilk adımı da bu.
  • RxJS 9'u Angular dışında, ayrı bir deneme projesinde denemek. Bu yazıdaki rxjs9-trial tam olarak bu.
  • Yükseltme zamanı Angular'ın peer aralığına RxJS 9 eklendiği gün başlar. eslint-plugin-rxjs-x 1.0.6'nın peer aralığı da şimdilik rxjs ^7.2.0; lint adımı da o gün yeniden kontrol edilmeli.

Üretimde Dikkat

Beta bir sürüm hakkında yazılan her şey kısa ömürlü. Bu yazıdaki bilgiler 14 Kasım 2026'da npm'den (npm view rxjs dist-tags, npm view @angular/core peerDependencies) ve paketin kendi dokümanlarından kontrol edildi; RxJS 9 kararlı sürüme çıkmadan önce API'nin değişmesi muhtemel. Bir karar vermeden önce bu iki komutu yeniden çalıştır.

Native Observable'a dayanan bir kod yazacaksan, bugün Firefox ve Safari'de olmadığını unutma. RxJS 9 polyfill'i bu boşluğu kendisi dolduruyor, ama doğrudan element.when('click') yazan ve RxJS import etmeyen bir kod bu tarayıcılarda TypeError verir. Baseline durumu değişene kadar özellik kontrolü yap ya da bir polyfill yükle.

RxJS 7 içindeki taşımaları da küçük adımlarla yap: önce lint kurallarıyla listeyi çıkar, sonra her publish/multicast kullanımını, davranışını sabitleyen bir testle birlikte değiştir. Bu yazıdaki publishReplay örneği, testsiz bir taşımanın hata durumunda davranışı nasıl sessizce değiştirebildiğini gösteriyor.

Sık Düşülen Tuzaklar

  • toPromise()'i lastValueFrom ile değiştirip boş akışı düşünmemek. Eskiden undefined dönen kod artık EmptyError ile reject olur; gerekiyorsa defaultValue ver.
  • publishReplay(1), refCount()'u düşünmeden shareReplay({ bufferSize: 1, refCount: true }) yapmak. Hata sonrası davranış değişir.
  • Bundle boyutunu ES5 build'iyle ölçmek. Bundler RxJS 7'nin es2015 build'ini seçmezse sonuç yanıltıcı olur.
  • RxJS 9 beta'yı peer uyarısını yok sayarak bir Angular projesine kurmak. Build, Angular'ın kendi rxjs/operators import'larında kırılır.
  • RxJS 9'da her Observable'ın cold olduğunu varsaymak. Platform Observable aynı anda subscribe olanlar arasında producer'ı paylaşır; her subscriber'a ayrı producer gerekiyorsa ColdObservable kullan.
  • Bir migration aracının çıktısını derlemeden ve testleri çalıştırmadan kabul etmek. @rxjs/migrate'in çıktısı bizim denememizde olduğu gibi derlenmedi.
  • Native EventTarget.when()'i bütün tarayıcılarda var saymak. Kasım 2026 itibarıyla yalnız Chromium tabanlı tarayıcılarda var.

Küçük Bir Deneme

★☆☆ bookstore-web'de npx eslint .'i, recommended config'teki rxjs-x/no-topromise ve rxjs-x/prefer-root-operators kuralları açıkken çalıştır. Sonra bir dosyaya import { map } from 'rxjs/operators' ve bir toPromise() çağrısı ekle; kuralların hangisi otomatik düzeltme (--fix) öneriyor?

★★☆ 02-multicast-migration.ts'teki publishReplay senaryosunu publishLast() + refCount() için tekrarla ve RxJS'in deprecation rehberindeki karşılığıyla (share ve AsyncSubject) aynı çıktıyı verdiğini, hata durumu dahil, göster.

★★★ rxjs9-trial'da error handling yazısındaki retryWithBackoff'u RxJS 9 ile yaz: rxjs/retry'ın imzasına bak, @rxjs/test ile bir önceki yazıdaki üç retry testini (500/1000/2000 ms bekleme, 404'te retry yok, üçüncü denemede başarı) aynı marble'larla geçir. Marble'larda neyi değiştirmen gerekti?

RxJS serisi burada bitiyor. Seri boyunca kurduğumuz kural RxJS 9'dan sonra da geçerli: zaman ve event akışı RxJS'te, ekranın gösterdiği state signal'da. Uygulama ölçeğinde state'i (Signal Store, rxMethod, NgRx Effects) Angular'da State Yönetimi serisinde, WebSocket ve SSE (Server-Sent Events; sunucunun tarayıcıya tek yönlü event gönderdiği HTTP bağlantısı) gibi gerçek zamanlı kaynakları Real-time Uygulamalar serisinde, LLM cevaplarını token token akıtan arayüzleri de Angular ile AI Arayüzleri serisinde bu temel üzerine kuruyoruz.