Higher-order Mapping: switchMap, mergeMap, concatMap ve exhaustMap Arasında Doğru Seçim
Arama gibi yalnız son değerin önemli olduğu okumalarda switchMap, sırası önemli yazmalarda concatMap, tekrarlanmaması gereken aksiyonlarda exhaustMap, bağımsız işlerde sınırlı mergeMap seçilir. Dört operator'ü aynı marble ile karşılaştırıyor, Bookstore'un arama kutusu, sepet adedi, çift tıklanan Siparişi Ver butonu ve 50 kapak indirmesiyle sanal zamanda çalıştırıyor, switchMap'in POST'ta neden tehlikeli olduğunu gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler RxJS 7.8.2, TypeScript 6.0 ve Node.js 24 ile denenmiştir; zamanlı örnekler RxJS'in TestScheduler'ı ile sanal zamanda çalıştırılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) arama kutusuna kullanıcı 150 ms arayla "du", "dun" ve "dune" yazıyor. Her terim bir arama isteği başlatıyor. Kısa terim daha çok kitapla eşleştiği için API onu daha yavaş cevaplıyor: "du" 600 ms, "dun" 200 ms, "dune" 100 ms sürüyor. Kod şöyle:
terms$.pipe(mergeMap((term) => searchBooks(term))).subscribe(showResults);
İstekler 0, 150 ve 300 ms'de gidiyor. Ekranda en son hangi terimin sonuçları kalır? mergeMap yerine switchMap yazsaydık ne değişirdi?
Önceki yazıdaki operator'lerin hepsi bir değeri başka bir değere çeviriyordu. Bu yazıda bir değerin bir Observable başlattığı durumlara bakıyoruz: arama terimi bir HTTP isteğine, "Siparişi Ver" tıklaması bir POST'a, kapak listesi elli görsel indirmesine dönüşüyor. Bunun için RxJS'te dört temel operator var: switchMap, mergeMap, concatMap ve exhaustMap. Hangisini seçtiğin, kullanıcının ekranda ne gördüğünü ve sunucuda ne olduğunu doğrudan belirliyor.
Inner Observable ve Flatten Stratejisi
map((term) => searchBooks(term)) yazarsan elinde arama sonuçları değil, her terim için bir Observable olur: Observable<Observable<SearchResult>>. Dıştaki akışa outer, her değerin ürettiği akışa inner Observable denir. Higher-order mapping operator'leri inner Observable'a kendileri subscribe olup değerlerini tek bir akışa "düzleştirir" (flatten). Aralarındaki tek fark şu sorunun cevabı: önceki inner Observable henüz bitmemişken yeni bir outer değer gelirse ne olacak?
| Operator | Yeni değer gelince | Tipik kullanım |
|---|---|---|
switchMap | Öncekini iptal eder (unsubscribe), yenisine geçer | Arama, filtre, route parametresine göre veri çekme |
mergeMap | Öncekiyle paralel çalıştırır; concurrent ile sınırlanabilir | Birbirinden bağımsız işler: toplu indirme, loglama |
concatMap | Kuyruğa koyar, önceki bitince başlatır | Sırası önemli yazma işlemleri |
exhaustMap | Önceki bitene kadar yeni değeri yok sayar | Çift tıklama, login, "Siparişi Ver" |
Aynı girdiyle dördünü yan yana görelim. Önceki yazıdaki showMarbles yardımcı fonksiyonu burada da çıktıyı RxJS'in TestScheduler'ında üretip doğruluyor. Outer akış 1. frame'de a, 4. frame'de b yayıyor; her biri iki değer yayan ve 7 frame süren bir inner Observable başlatıyor (a'nınki A, b'ninki B yayıyor):
import { concatMap, exhaustMap, mergeMap, switchMap } from "rxjs";
import { showMarbles } from "../../tools/marble.ts";
// dört operator için aynı outer akış ve aynı inner Observable:
// a değeri iki kez "A" yayan, b değeri iki kez "B" yayan bir inner akış başlatır
const outer = "-a--b---------|";
const inner = "--X---X|";
showMarbles("mergeMap", (cold) => cold<string>(outer).pipe(mergeMap((v) => cold(inner, { X: v.toUpperCase() }))));
showMarbles("concatMap", (cold) => cold<string>(outer).pipe(concatMap((v) => cold(inner, { X: v.toUpperCase() }))));
showMarbles("switchMap", (cold) => cold<string>(outer).pipe(switchMap((v) => cold(inner, { X: v.toUpperCase() }))));
showMarbles("exhaustMap", (cold) => cold<string>(outer).pipe(exhaustMap((v) => cold(inner, { X: v.toUpperCase() }))));
mergeMap
in : -a--b---------|
in : --X---X| X=A
in : --X---X| X=B
out: ---A--BA--B---|
concatMap
in : -a--b---------|
in : --X---X| X=A
in : --X---X| X=B
out: ---A---A--B---B|
switchMap
in : -a--b---------|
in : --X---X| X=A
in : --X---X| X=B
out: ---A--B---B---|
exhaustMap
in : -a--b---------|
in : --X---X| X=A
out: ---A---A------|
Dört "out" satırı dört stratejinin özeti. mergeMap'te A ve B'ler iç içe geçiyor: iki inner akış aynı anda çalışıyor. concatMap'te B'ler ancak a'nın inner akışı bittikten sonra (8. frame) başlıyor, bu yüzden çıktı en geç biten satır. switchMap'te b gelince a'nın inner akışı iptal edildi; ikinci A hiç gelmedi. exhaustMap'te ise tersi oldu: a'nın inner akışı sürerken gelen b yok sayıldı ve hiç B üretilmedi. Hiçbir operator değer kaybetmiyor diyemeyiz; switchMap ve exhaustMap bilerek kaybediyor, soru hangisinin senin senaryonda doğru kayıp olduğu.
Zamanlı Örnekleri Sanal Zamanda Çalıştırmak
Bu yazının senaryoları gerçek zamanda saniyeler sürerdi ve her koşuda milisaniyeler biraz oynardı. Bunun yerine snapshot'taki tools/virtual-time.ts her senaryoyu TestScheduler'ın run modunda çalıştırıyor. Bu modda timer, delay gibi zamana bağlı her şey sanal milisaniye kullanıyor: 5 saniyelik bir senaryo anında bitiyor ve her koşuda aynı zamanları yazıyor. log satır başına o anki sanal zamanı ekliyor, emitAt verilen anlarda değer yayan bir Observable kuruyor. Sahte HTTP çağrısı da şöyle:
// Sanal zamanlı örnekler için sahte bir HTTP çağrısı: isteği loglar, latencyMs sonra cevap verir,
// subscriber cevaptan önce unsubscribe olursa iptali loglar.
// onServer (opsiyonel): istek ulaşır ulaşmaz sunucunun yaptığı iş.
import { Observable, timer } from "rxjs";
import { log } from "../../tools/virtual-time.ts";
export function fakeRequest<T>(label: string, latencyMs: number, answer: () => T, onServer?: () => void): Observable<T> {
return new Observable<T>((subscriber) => {
log(`-> ${label}`);
onServer?.();
let answered = false;
const pending = timer(latencyMs).subscribe(() => {
answered = true;
log(`<- ${label}`);
subscriber.next(answer());
subscriber.complete();
});
return () => {
if (!answered) log(`x ${label} cancelled`);
pending.unsubscribe();
};
});
}
İlk yazıda yazdığımız getStock'un sanal zamanlı kardeşi: -> istek gitti, <- cevap geldi, x istek cevap gelmeden iptal edildi demek. Teardown'daki iptal logu, Angular'ın HttpClient'ında unsubscribe'ın isteği iptal etmesinin karşılığı.
Arama Kutusu: switchMap
Tahmindeki senaryoyu dört operator'le de çalıştıralım:
import { concatMap, exhaustMap, mergeMap, switchMap, type OperatorFunction } from "rxjs";
import { books } from "../../data/books.ts";
import { emitAt, log, runInVirtualTime } from "../../tools/virtual-time.ts";
import { fakeRequest } from "./fake-api.ts";
type SearchResult = { term: string; titles: string[] };
// kısa terim daha çok kitapla eşleşir, API onu daha yavaş cevaplar
const latency: Record<string, number> = { du: 600, dun: 200, dune: 100 };
function searchBooks(term: string) {
return fakeRequest(`GET /books?q=${term}`, latency[term], (): SearchResult => ({
term,
titles: books.filter((b) => b.title.toLowerCase().includes(term)).map((b) => b.title),
}));
}
function runSearch(name: string, flatten: OperatorFunction<string, SearchResult>) {
runInVirtualTime(name, () => {
// kullanıcı 150 ms arayla "du", "dun", "dune" yazıyor
const terms$ = emitAt([0, "du"], [150, "dun"], [300, "dune"]);
terms$.pipe(flatten).subscribe((r) => log(`screen: "${r.term}" -> ${r.titles.join(", ")}`));
});
}
runSearch("mergeMap", mergeMap((term) => searchBooks(term)));
runSearch("concatMap", concatMap((term) => searchBooks(term)));
runSearch("switchMap", switchMap((term) => searchBooks(term)));
runSearch("exhaustMap", exhaustMap((term) => searchBooks(term)));
--- mergeMap
0 ms -> GET /books?q=du
150 ms -> GET /books?q=dun
300 ms -> GET /books?q=dune
350 ms <- GET /books?q=dun
350 ms screen: "dun" -> Dune
400 ms <- GET /books?q=dune
400 ms screen: "dune" -> Dune
600 ms <- GET /books?q=du
600 ms screen: "du" -> Dune
--- concatMap
0 ms -> GET /books?q=du
600 ms <- GET /books?q=du
600 ms screen: "du" -> Dune
600 ms -> GET /books?q=dun
800 ms <- GET /books?q=dun
800 ms screen: "dun" -> Dune
800 ms -> GET /books?q=dune
900 ms <- GET /books?q=dune
900 ms screen: "dune" -> Dune
--- switchMap
0 ms -> GET /books?q=du
150 ms x GET /books?q=du cancelled
150 ms -> GET /books?q=dun
300 ms x GET /books?q=dun cancelled
300 ms -> GET /books?q=dune
400 ms <- GET /books?q=dune
400 ms screen: "dune" -> Dune
--- exhaustMap
0 ms -> GET /books?q=du
600 ms <- GET /books?q=du
600 ms screen: "du" -> Dune
Tahminin cevabı mergeMap bloğunda: ekranda en son "du" sonuçları kaldı. Kullanıcı "dune" yazmış, ama en yavaş istek en son döndüğü için 600. ms'de ekran eski terimin sonuçlarıyla güncellendi. Bu bir race condition ve test ortamında genelde görünmüyor, çünkü yerel API'de istekler hep aynı hızda dönüyor. Bookstore'un 40 bin kitaplık kataloğunda geniş bir arama ile dar bir arama arasındaki süre farkı ise production'da tam olarak bu sırayı üretiyor.
concatMap doğru sonucu gösterdi ama 900 ms'de; kullanıcı artık ilgilenmediği iki aramanın bitmesini bekledi. exhaustMap en kötüsü: "du" bitene kadar gelen terimleri yok saydı ve "dune" hiç aranmadı. switchMap ise her yeni terimde önceki isteği iptal etti (x satırları) ve yalnız son terimin sonucunu 400 ms'de gösterdi. Arama, filtre ve route parametresi gibi "yalnız son değer önemli" senaryolarının operator'ü switchMap. Gerçek arama kutusunda bunun önüne bir debounceTime eklenir; zaman operator'leri bu serinin zaman yazısında.
Sepette Adet Değiştirmek: concatMap
Kullanıcı sepet satırında Dune'un adedini "+" ile üç kez artırıyor: 2, 3, 4. Her tıklama bir PUT isteği atıyor. Ağdaki dalgalanma yüzünden bu koşuda sonraki istekler daha hızlı cevaplanıyor:
import { concatMap, mergeMap, type OperatorFunction } from "rxjs";
import { emitAt, log, runInVirtualTime } from "../../tools/virtual-time.ts";
import { fakeRequest } from "./fake-api.ts";
// ağ dalgalanması: bu koşuda sonraki istekler tesadüfen daha hızlı cevaplanıyor
const latency: Record<number, number> = { 2: 300, 3: 200, 4: 100 };
function setQuantity(quantity: number) {
return fakeRequest(`PUT /cart/items/9780441172719 quantity=${quantity}`, latency[quantity], () => quantity);
}
function runStepper(name: string, flatten: OperatorFunction<number, number>) {
runInVirtualTime(name, () => {
// kullanıcı Dune'da üç kez "+"ya basıyor: 1 -> 2 -> 3 -> 4
const quantities$ = emitAt([0, 2], [50, 3], [100, 4]);
quantities$.pipe(flatten).subscribe((q) => log(`cart line shows quantity ${q}`));
});
}
runStepper("mergeMap", mergeMap((q) => setQuantity(q)));
runStepper("concatMap", concatMap((q) => setQuantity(q)));
--- mergeMap
0 ms -> PUT /cart/items/9780441172719 quantity=2
50 ms -> PUT /cart/items/9780441172719 quantity=3
100 ms -> PUT /cart/items/9780441172719 quantity=4
200 ms <- PUT /cart/items/9780441172719 quantity=4
200 ms cart line shows quantity 4
250 ms <- PUT /cart/items/9780441172719 quantity=3
250 ms cart line shows quantity 3
300 ms <- PUT /cart/items/9780441172719 quantity=2
300 ms cart line shows quantity 2
--- concatMap
0 ms -> PUT /cart/items/9780441172719 quantity=2
300 ms <- PUT /cart/items/9780441172719 quantity=2
300 ms cart line shows quantity 2
300 ms -> PUT /cart/items/9780441172719 quantity=3
500 ms <- PUT /cart/items/9780441172719 quantity=3
500 ms cart line shows quantity 3
500 ms -> PUT /cart/items/9780441172719 quantity=4
600 ms <- PUT /cart/items/9780441172719 quantity=4
600 ms cart line shows quantity 4
mergeMap ile sepet satırı 4 → 3 → 2 diye geriye saydı ve 2'de kaldı; kullanıcı 4 istemişti. Daha kötüsü, gerçek bir ağda paralel giden üç PUT'un sunucuya hangi sırayla ulaşacağı da garanti değil, yani sunucudaki değer de 2 ya da 3 olabilir. concatMap her isteği öncekinin cevabından sonra gönderdi: istekler sırayla gitti, ekran 2, 3, 4 diye ilerledi ve sunucu ile ekran aynı değerde kaldı. Bedeli süre: son değer 600 ms'de geldi. Sırası önemli yazma işlemlerinde (adet güncelleme, sıralı adımlar, aynı kaydı değiştiren ardışık PATCH'ler) bu bedel ödenir. concatMap, mergeMap(project, 1) ile aynı davranıyor; aradaki kuyruk bellekte tutulduğu için kullanıcı çok hızlı tıklarsa kuyruk da büyür.
"Siparişi Ver" Çift Tıklama: exhaustMap
Ada, 770 TL'lik siparişini (Dune ve Emma) veriyor ve butona alışkanlıkla çift tıklıyor, ardından bir kez daha basıyor. Sunucu siparişi istek ulaştığı anda oluşturuyor ve ödemeyi alıyor, cevabı 500 ms sonra dönüyor:
import { defer, exhaustMap, mergeMap, switchMap, type OperatorFunction } from "rxjs";
import { emitAt, log, runInVirtualTime } from "../../tools/virtual-time.ts";
import { fakeRequest } from "./fake-api.ts";
let nextOrderId = 1042;
// sunucu siparişi istek ulaşır ulaşmaz oluşturur, 500 ms sonra cevap verir
function placeOrder() {
return defer(() => {
const orderId = nextOrderId++;
return fakeRequest(`POST /orders (order ${orderId})`, 500, () => orderId, () =>
log(` server: order ${orderId} created for Ada, 770 TRY charged`),
);
});
}
function runCheckout(name: string, flatten: OperatorFunction<string, number>) {
nextOrderId = 1042;
runInVirtualTime(name, () => {
// Ada "Place Order"a çift tıklıyor, sonra bir kez daha basıyor
const clicks$ = emitAt([0, "click"], [80, "click"], [160, "click"]);
clicks$.pipe(flatten).subscribe((id) => log(`screen: thank you, order ${id}`));
});
}
runCheckout("mergeMap", mergeMap(() => placeOrder()));
runCheckout("switchMap", switchMap(() => placeOrder()));
runCheckout("exhaustMap", exhaustMap(() => placeOrder()));
--- mergeMap
0 ms -> POST /orders (order 1042)
0 ms server: order 1042 created for Ada, 770 TRY charged
80 ms -> POST /orders (order 1043)
80 ms server: order 1043 created for Ada, 770 TRY charged
160 ms -> POST /orders (order 1044)
160 ms server: order 1044 created for Ada, 770 TRY charged
500 ms <- POST /orders (order 1042)
500 ms screen: thank you, order 1042
580 ms <- POST /orders (order 1043)
580 ms screen: thank you, order 1043
660 ms <- POST /orders (order 1044)
660 ms screen: thank you, order 1044
--- switchMap
0 ms -> POST /orders (order 1042)
0 ms server: order 1042 created for Ada, 770 TRY charged
80 ms x POST /orders (order 1042) cancelled
80 ms -> POST /orders (order 1043)
80 ms server: order 1043 created for Ada, 770 TRY charged
160 ms x POST /orders (order 1043) cancelled
160 ms -> POST /orders (order 1044)
160 ms server: order 1044 created for Ada, 770 TRY charged
660 ms <- POST /orders (order 1044)
660 ms screen: thank you, order 1044
--- exhaustMap
0 ms -> POST /orders (order 1042)
0 ms server: order 1042 created for Ada, 770 TRY charged
500 ms <- POST /orders (order 1042)
500 ms screen: thank you, order 1042
mergeMap ile Ada'nın kartından üç kez 770 TL çekildi ve ekranda üç teşekkür mesajı göründü. switchMap daha sinsi: ekranda tek bir sipariş (1044) görünüyor, her şey doğru çalışmış gibi. Ama sunucu logunda üç sipariş var. İstemci ilk iki isteği iptal etti, yani cevaplarını dinlemeyi bıraktı; sunucu ise istekleri çoktan almış ve işlemişti. Unsubscribe, gönderilmiş bir HTTP isteğini geri almaz, en fazla cevabın beklenmesini durdurur. Bu yüzden POST, PUT, DELETE gibi yan etkisi olan isteklerde switchMap tehlikeli: kullanıcı hiçbir şey görmeden sunucuda yarım kalmış ya da tekrarlanmış iş bırakır.
exhaustMap ilk tıklamayı işleme aldı, istek sürerken gelen iki tıklamayı yok saydı: tek sipariş, tek ödeme. "Siparişi Ver", "Giriş Yap", "Ödemeyi Onayla" gibi işlem sürerken tekrarlanmaması gereken her aksiyonun operator'ü bu. Yine de tek savunma hattı olarak bırakma: başka sekmeden, retry'dan ya da ağ kesintisinden gelen tekrarları sunucu tarafında bir idempotency key ile engellemek ayrıca gerekiyor; bu mekanizma Çözüm Desenleri serisinin "Idempotency Key: Çift Tıklama ve Retry'da Aynı Siparişi İki Kez Oluşturmamak" yazısında anlatıldı.
50 Kapak, En Fazla 4 Paralel: mergeMap(…, 4)
Yönetim ekranında 50 kitabın kapak görseli yükleniyor. İşler birbirinden bağımsız, sıra önemli değil; bu mergeMap'in alanı. Ama sınırsız paralellik de sorun:
import { concatMap, defer, finalize, map, mergeMap, range, timer, type OperatorFunction } from "rxjs";
import { log, runInVirtualTime } from "../../tools/virtual-time.ts";
let active = 0;
let maxActive = 0;
// bir kapak görselinin yüklenmesi 100 ms sürüyor; active/maxActive paralel indirmeleri sayar
function loadCover(index: number) {
return defer(() => {
active++;
maxActive = Math.max(maxActive, active);
return timer(100).pipe(
map(() => `cover-${index}.jpg`),
finalize(() => active--),
);
});
}
function runCovers(name: string, flatten: OperatorFunction<number, string>) {
active = 0;
maxActive = 0;
runInVirtualTime(name, () => {
let loaded = 0;
range(1, 50)
.pipe(flatten)
.subscribe({
next: () => loaded++,
complete: () => log(`${loaded} covers loaded, at most ${maxActive} at the same time`),
});
});
}
runCovers("mergeMap (no limit)", mergeMap((i) => loadCover(i)));
runCovers("mergeMap(..., 4)", mergeMap((i) => loadCover(i), 4));
runCovers("concatMap", concatMap((i) => loadCover(i)));
--- mergeMap (no limit)
100 ms 50 covers loaded, at most 50 at the same time
--- mergeMap(..., 4)
1300 ms 50 covers loaded, at most 4 at the same time
--- concatMap
5000 ms 50 covers loaded, at most 1 at the same time
Sınırsız mergeMap 50 isteği aynı anda başlattı ve sanal zamanda 100 ms'de bitti. Gerçek dünyada bu rakam yanıltıcı: tarayıcı HTTP/1.1'de aynı host'a sınırlı sayıda bağlantı açtığı için istekler zaten tarayıcının kuyruğunda bekler, sunucu ve CDN (içerik dağıtım ağı) de aynı anda gelen 50 isteği rate limit'e takabilir. Bunun da ötesinde, kullanıcının gördüğü ilk kapaklar bu kalabalığın içinde yavaşlar. concatMap diğer uç: tek tek, 5 saniye. mergeMap'in ikinci parametresi concurrent ikisinin arasını veriyor: en fazla 4 paralel istek, 1,3 saniye. Doğru sayı ölçerek bulunur; sunucunun rate limit'i ve tarayıcının bağlantı davranışı (HTTP/2'de tek bağlantı üzerinden çoklu istek) bu sayıyı belirler.
Sayfalı API'yi Sonuna Kadar Çekmek: expand
Bazen bir sonraki isteğin parametresi bir önceki cevabın içinde gelir: sayfalı bir API "sonraki sayfa şu offset'ten başlar" der. expand, her değeri yayıp aynı zamanda o değerden yeni bir inner Observable üreten, yani kendini besleyen bir mergeMap:
import { EMPTY, expand, reduce } from "rxjs";
import { books } from "../../data/books.ts";
import { log, runInVirtualTime } from "../../tools/virtual-time.ts";
import { fakeRequest } from "./fake-api.ts";
type CatalogPage = { items: string[]; nextOffset: number | null };
// katalog API'si sayfa başına 2 kitap döner ve sonraki sayfanın nereden başladığını söyler
function getCatalogPage(offset: number) {
return fakeRequest(`GET /books?offset=${offset}&limit=2`, 100, (): CatalogPage => ({
items: books.slice(offset, offset + 2).map((b) => b.title),
nextOffset: offset + 2 < books.length ? offset + 2 : null,
}));
}
runInVirtualTime("expand", () => {
getCatalogPage(0)
.pipe(
expand((page) => (page.nextOffset === null ? EMPTY : getCatalogPage(page.nextOffset))),
reduce((all, page) => [...all, ...page.items], [] as string[]),
)
.subscribe((titles) => log(`${titles.length} books: ${titles.join(", ")}`));
});
--- expand
0 ms -> GET /books?offset=0&limit=2
100 ms <- GET /books?offset=0&limit=2
100 ms -> GET /books?offset=2&limit=2
200 ms <- GET /books?offset=2&limit=2
200 ms -> GET /books?offset=4&limit=2
300 ms <- GET /books?offset=4&limit=2
300 ms 6 books: Dune, Emma, The Hobbit, Middlemarch, Frankenstein, Pride and Prejudice
Durma koşulu EMPTY: son sayfada nextOffset null geldiğinde yeni istek üretilmiyor, inner akışlar bitince expand de tamamlanıyor ve önceki yazıdaki reduce bütün sayfaları tek bir listede topluyor. Bu, reduce'un doğru yeri: akış gerçekten bitiyor. Durma koşulunu unutursan (ya da API son sayfada yanlışlıkla bir offset dönerse) expand sonsuza kadar istek atar; production'da bir üst sınır (take(100) gibi) koymak ucuz bir sigorta.
Üretimde Dikkat
Yanlış flatten stratejisinin belirtileri genelde "ara sıra" olur, bu yüzden ölçmek gerekiyor. Arama kutusunda DevTools'un Network sekmesinde aynı anda kaç arama isteğinin beklediğine bak; switchMap ile iptal edilen istekler "(canceled)" durumuyla görünür, birikmiş pending istekler ise yanlış operator'ün işaretidir. Ağı yavaşlatmak (Network throttling ile "Slow 4G") race condition'ı geliştirici makinesinde de görünür yapar.
Yazma işlemlerinde asıl ölçü sunucu tarafında: aynı müşteri için birkaç yüz milisaniye arayla oluşan iki sipariş, ya da sırası karışmış güncelleme logları. Bookstore'un sipariş tablosunda "aynı müşteri, aynı tutar, 1 saniyeden kısa arayla" oluşan siparişleri sayan basit bir rapor, istemcideki yanlış bir mergeMap'i ya da eksik bir exhaustMap'i hemen gösterir.
Seçim için kısa bir kural: önce işin okuma mı yazma mı olduğuna bak. Okuma işlemlerinde (GET) iptal güvenlidir, bu yüzden "yalnız son değer önemli" ise switchMap, hepsi önemliyse sınırlı mergeMap. Yazma işlemlerinde iptal güvenli değildir: sıra önemliyse concatMap, tekrar istenmiyorsa exhaustMap. switchMap'i yazma işlemine koymadan önce sunucunun yarım kalan bir isteği nasıl ele aldığını bildiğinden emin ol.
Sık Düşülen Tuzaklar
- Arama ya da filtrede
mergeMapkullanmak. Cevaplar sırasız döner, en yavaş eski istek ekranı son günceller. - POST, PUT, DELETE isteklerinde
switchMapkullanmak. İptal yalnız istemci tarafındadır; sunucu isteği çoktan işlemiş olabilir. - Sınırsız
mergeMapile yüzlerce istek başlatmak.concurrentparametresiyle bir üst sınır koy. - Arama kutusunda
exhaustMapya daconcatMapkullanmak. Kullanıcının son yazdığı terim ya hiç aranmaz ya da artık ilgilenmediği aramaların bitmesini bekler. expand'de durma koşulunu (EMPTY) unutmak ya da yalnız API'nin doğru cevap vereceğine güvenmek.- Higher-order operator yerine
subscribeiçindesubscribeyazmak. İç subscription'ı kimse iptal etmez ve hangi stratejinin uygulandığı koddan okunmaz; bu,mergeMap'in sınırsız ve kontrolsüz hâlidir.
Küçük Bir Deneme
★☆☆ 01-search.ts'te gecikmeleri { du: 100, dun: 200, dune: 600 } yap. Dört operator'ün çıktısını önce tahmin et, sonra çalıştır. Hangi operator'ün sonucu değişti ve neden switchMap'inki değişmedi?
★★☆ 04-place-order.ts'e dördüncü bir senaryo ekle: concatMap. Ada'nın üç tıklamasında sunucuda kaç sipariş oluşur? Sonra exhaustMap'e, istek sürerken butonu devre dışı bırakmak için bir log("button disabled") ve log("button enabled") ekle; bunları akışın neresine koyduğunu ve neden subscribe içine koymadığını açıkla.
★★★ 05-covers.ts'teki loadCover'ı, her 10. kapakta 300 ms süren "büyük görsel" olacak şekilde değiştir. mergeMap(…, 4) ile toplam süre ve kapakların yüklenme sırası nasıl değişiyor? Kapakları ekranda yine 1'den 50'ye sırayla göstermek istesen hangi operator'ü seçerdin ve paralellikten ne kaybederdin?
Bu yazıda tek bir akışın her değerinden yeni bir akış ürettik. Bir sonraki yazıda farklı akışları birleştireceğiz: katalog sayfasındaki kategori, fiyat aralığı ve sıralama filtrelerini combineLatest ile tek bir sorguya çevirecek, sipariş detayında üç API çağrısını forkJoin ile paralel yapacak ve withLatestFrom, zip, merge, concat ve race'in hangi soruya cevap verdiğine bakacağız.