Node.js'te AsyncLocalStorage ile istek kapsamlı değişken yönetiminde ustalaşın. Prop drilling'i ortadan kaldırın ve küresel kitleler için daha temiz, gözlemlenebilir uygulamalar oluşturun.
JavaScript Asenkron Bağlamının Kilidini Açmak: İstek Kapsamlı Değişken Yönetimine Derinlemesine Bir Bakış
Modern sunucu taraflı geliştirme dünyasında, durumu (state) yönetmek temel bir zorluktur. Node.js ile çalışan geliştiriciler için bu zorluk, tek iş parçacıklı, engellemeyen, asenkron doğası nedeniyle daha da artar. Bu model, yüksek performanslı, G/Ç (I/O) ağırlıklı uygulamalar oluşturmak için inanılmaz derecede güçlü olsa da, benzersiz bir sorunu da beraberinde getirir: ara yazılımlardan (middleware) veritabanı sorgularına ve üçüncü taraf API çağrılarına kadar çeşitli asenkron operasyonlardan geçerken belirli bir istek için bağlamı nasıl korursunuz? Bir kullanıcının isteğinden gelen verilerin diğerine sızmamasını nasıl sağlarsınız?
Yıllarca JavaScript topluluğu bu sorunla boğuştu ve genellikle kullanıcı kimliği veya izleme kimliği gibi isteğe özgü verileri bir çağrı zincirindeki her bir fonksiyondan geçirmek anlamına gelen "prop drilling" gibi hantal desenlere başvurdu. Bu yaklaşım kodu karmaşıklaştırır, modüller arasında sıkı bir bağ oluşturur ve bakımı sürekli bir kabusa dönüştürür.
İşte bu noktada, bu uzun süredir devam eden soruna sağlam bir çözüm sunan bir kavram olan Asenkron Bağlam devreye giriyor. Node.js'te kararlı AsyncLocalStorage API'sinin tanıtılmasıyla, geliştiriciler artık istek kapsamlı değişkenleri zarif ve verimli bir şekilde yönetmek için güçlü, yerleşik bir mekanizmaya sahipler. Bu kılavuz, sizi JavaScript asenkron bağlamı dünyasında kapsamlı bir yolculuğa çıkaracak, sorunu açıklayacak, çözümü tanıtacak ve küresel bir kullanıcı kitlesi için daha ölçeklenebilir, sürdürülebilir ve gözlemlenebilir uygulamalar oluşturmanıza yardımcı olacak pratik, gerçek dünya örnekleri sunacaktır.
Temel Zorluk: Eşzamanlı, Asenkron Bir Dünyada Durum (State) Yönetimi
Çözümü tam olarak takdir etmek için önce sorunun derinliğini anlamalıyız. Bir Node.js sunucusu binlerce eşzamanlı isteği işler. A İsteği geldiğinde, Node.js onu işlemeye başlayabilir, ardından bir veritabanı sorgusunun tamamlanmasını beklemek için duraklayabilir. Beklerken, B İsteğini alır ve onun üzerinde çalışmaya başlar. A İsteği için veritabanı sonucu döndüğünde, Node.js yürütmesine devam eder. Bu sürekli bağlam değiştirme, performansının ardındaki sihirdir, ancak geleneksel durum yönetimi tekniklerini alt üst eder.
Global Değişkenler Neden Başarısız Olur
Acemi bir geliştiricinin ilk içgüdüsü global bir değişken kullanmak olabilir. Örneğin:
let currentUser; // Global bir değişken
// Kullanıcıyı ayarlamak için ara yazılım (middleware)
app.use((req, res, next) => {
currentUser = await getUserFromDb(req.headers.authorization);
next();
});
// Uygulamanın derinliklerinde bir servis fonksiyonu
function logActivity() {
console.log(`Activity for user: ${currentUser.id}`);
}
Bu, eşzamanlı bir ortamda feci bir tasarım hatasıdır. Eğer A İsteği currentUser değişkenini ayarlar ve ardından bir asenkron işlemi beklerse, B İsteği gelip A İsteği bitmeden currentUser değişkeninin üzerine yazabilir. A İsteği devam ettiğinde, yanlışlıkla B İsteğinden gelen verileri kullanacaktır. Bu, öngörülemeyen hatalara, veri bozulmasına ve güvenlik açıklarına yol açar. Global değişkenler istek-güvenli (request-safe) değildir.
Prop Drilling'in Sancısı
Daha yaygın ve daha güvenli olan geçici çözüm, "prop drilling" veya "parametre geçirme" olmuştur. Bu, bağlamı ihtiyaç duyan her fonksiyona açıkça bir argüman olarak geçirmeyi içerir.
Uygulamamız boyunca loglama için benzersiz bir traceId ve yetkilendirme için bir user nesnesine ihtiyacımız olduğunu hayal edelim.
Prop Drilling Örneği:
// 1. Giriş noktası: Ara yazılım (Middleware)
app.use((req, res, next) => {
const traceId = generateTraceId();
const user = { id: 'user-123', locale: 'en-GB' };
const requestContext = { traceId, user };
processOrder(requestContext, req.body.orderId);
});
// 2. İş mantığı katmanı
function processOrder(context, orderId) {
log('Processing order', context);
const orderDetails = getOrderDetails(context, orderId);
// ... daha fazla mantık
}
// 3. Veri erişim katmanı
function getOrderDetails(context, orderId) {
log(`Fetching order ${orderId}`, context);
return db.query('SELECT * FROM orders WHERE id = ?', orderId);
}
// 4. Yardımcı katman
function log(message, context) {
console.log(`[${context.traceId}] [User: ${context.user.id}] - ${message}`);
}
Bu yöntem işe yarasa ve eşzamanlılık sorunlarına karşı güvenli olsa da, önemli dezavantajları vardır:
- Kod Karmaşası:
contextnesnesi, doğrudan kullanmayan ancak çağırdıkları fonksiyonlara aşağı doğru geçirmesi gereken fonksiyonlar aracılığıyla bile her yere geçirilir. - Sıkı Bağlılık (Tight Coupling): Her fonksiyon imzası artık
contextnesnesinin şekline bağlıdır. Bağlama yeni bir veri parçası (örneğin, bir A/B testi bayrağı) eklemeniz gerekirse, kod tabanınızdaki düzinelerce fonksiyon imzasını değiştirmeniz gerekebilir. - Azaltılmış Okunabilirlik: Bir fonksiyonun birincil amacı, bağlamı etrafta dolaştırmanın standart kodları tarafından gölgede bırakılabilir.
- Bakım Yükü: Yeniden düzenleme (refactoring) sıkıcı ve hataya açık bir süreç haline gelir.
Daha iyi bir yola ihtiyacımız vardı. İstek-özel verileri tutan, o isteğin asenkron çağrı zinciri içinde herhangi bir yerden açıkça geçirilmeden erişilebilen "sihirli" bir kapsayıcıya sahip olmanın bir yoluna.
Karşınızda `AsyncLocalStorage`: Modern Çözüm
Node.js v13.10.0'dan beri kararlı bir özellik olan AsyncLocalStorage sınıfı, bu soruna resmi yanıttır. Geliştiricilerin, belirli bir giriş noktasından başlatılan tüm asenkron operasyonlar zinciri boyunca devam eden yalıtılmış bir depolama bağlamı oluşturmasına olanak tanır.
Bunu, JavaScript'in asenkron, olay güdümlü dünyası için bir tür "iş parçacığı-yerel depolama" (thread-local storage) olarak düşünebilirsiniz. Bir AsyncLocalStorage bağlamı içinde bir işlem başlattığınızda, o noktadan itibaren çağrılan herhangi bir fonksiyon —ister senkron, ister geri arama (callback) tabanlı, ister promise tabanlı olsun— o bağlamda depolanan verilere erişebilir.
Temel API Kavramları
API dikkate değer ölçüde basit ve güçlüdür. Üç anahtar yöntem etrafında döner:
new AsyncLocalStorage(): Depo'nun yeni bir örneğini oluşturur. Genellikle her bağlam türü için bir örnek oluşturursunuz (örneğin, tüm HTTP istekleri için bir tane) ve bunu uygulamanız boyunca paylaşırsınız.als.run(store, callback): Bu işin ağır yükünü çeker. Bir fonksiyonu (callback) çalıştırır ve yeni bir asenkron bağlam oluşturur. İlk argüman olanstore, o bağlam içinde kullanılabilir hale getirmek istediğiniz veridir.callbackiçinde yürütülen asenkron işlemler de dahil olmak üzere herhangi bir kod, bustore'a erişebilecektir.als.getStore(): Bu yöntem, mevcut bağlamdan veriyi (store) almak için kullanılır.run()tarafından oluşturulan bir bağlamın dışında çağrılırsa,undefineddöndürür.
Pratik Uygulama: Adım Adım Bir Kılavuz
Önceki prop-drilling örneğimizi AsyncLocalStorage kullanarak yeniden düzenleyelim. Standart bir Express.js sunucusu kullanacağız, ancak prensip herhangi bir Node.js çerçevesi veya hatta yerel http modülü için aynıdır.
Adım 1: Merkezi Bir `AsyncLocalStorage` Örneği Oluşturun
Deponuzun tek, paylaşılan bir örneğini oluşturmak ve uygulamanız boyunca kullanılabilmesi için dışa aktarmak en iyi uygulamadır. asyncContext.js adında bir dosya oluşturalım.
// asyncContext.js
import { AsyncLocalStorage } from 'async_hooks';
export const requestContextStore = new AsyncLocalStorage();
Adım 2: Bir Ara Yazılım (Middleware) ile Bağlamı Kurun
Bağlamı başlatmak için en ideal yer, bir isteğin yaşam döngüsünün en başıdır. Bir ara yazılım (middleware) bunun için mükemmeldir. İsteğe özgü verilerimizi oluşturacak ve ardından istek işleme mantığının geri kalanını als.run() içine saracağız.
// server.js
import express from 'express';
import { requestContextStore } from './asyncContext.js';
import { v4 as uuidv4 } from 'uuid'; // Benzersiz bir traceId oluşturmak için
const app = express();
// Sihirli ara yazılım
app.use((req, res, next) => {
const traceId = req.headers['x-request-id'] || uuidv4();
const user = { id: 'user-123', locale: 'en-GB' }; // Gerçek bir uygulamada bu, bir kimlik doğrulama ara yazılımından gelir
const store = { traceId, user };
// Bu istek için bağlamı kur
requestContextStore.run(store, () => {
next();
});
});
// ... rotalarınız ve diğer ara yazılımlarınız buraya gelir
Bu ara yazılımda, her gelen istek için traceId ve user içeren bir store nesnesi oluşturuyoruz. Ardından requestContextStore.run(store, ...) çağrısını yapıyoruz. İçindeki next() çağrısı, bu özel istek için sonraki tüm ara yazılımların ve rota işleyicilerinin bu yeni oluşturulan bağlam içinde yürütülmesini sağlar.
Adım 3: Bağlama Prop Drilling Olmadan Her Yerden Erişin
Şimdi, diğer modüllerimiz kökten basitleştirilebilir. Artık bir context parametresine ihtiyaçları yok. Sadece requestContextStore'umuzu içe aktarıp getStore()'u çağırabilirler.
Yeniden Düzenlenmiş Loglama Yardımcısı:
// logger.js
import { requestContextStore } from './asyncContext.js';
export function log(message) {
const context = requestContextStore.getStore();
if (context) {
const { traceId, user } = context;
console.log(`[${traceId}] [User: ${user.id}] - ${message}`);
} else {
// Bir istek bağlamı dışındaki loglar için geri dönüş
console.log(`[NO_CONTEXT] - ${message}`);
}
}
Yeniden Düzenlenmiş İş ve Veri Katmanları:
// orderService.js
import { log } from './logger.js';
import * as db from './database.js';
export function processOrder(orderId) {
log('Processing order'); // Bağlama gerek yok!
const orderDetails = getOrderDetails(orderId);
// ... daha fazla mantık
}
function getOrderDetails(orderId) {
log(`Fetching order ${orderId}`); // Logger bağlamı otomatik olarak alacak
return db.query('SELECT * FROM orders WHERE id = ?', orderId);
}
Fark gece ve gündüz gibi. Kod dramatik bir şekilde daha temiz, daha okunabilir ve bağlamın yapısından tamamen ayrılmış durumda. Loglama yardımcımız, iş mantığımız ve veri erişim katmanlarımız artık saf ve kendi özel görevlerine odaklanmış durumda. İstek bağlamımıza yeni bir özellik eklememiz gerekirse, sadece oluşturulduğu ara yazılımı değiştirmemiz yeterli. Başka hiçbir fonksiyon imzasının dokunulmasına gerek yok.
İleri Düzey Kullanım Senaryoları ve Küresel Bir Bakış Açısı
İstek kapsamlı bağlam sadece loglama için değildir. Gelişmiş, küresel uygulamalar oluşturmak için gerekli olan çeşitli güçlü desenlerin kilidini açar.
1. Dağıtık İzleme ve Gözlemlenebilirlik
Bir mikroservis mimarisinde, tek bir kullanıcı eylemi birden fazla servis arasında bir istek zincirini tetikleyebilir. Sorunları ayıklamak için bu tüm yolculuğu izleyebilmeniz gerekir. AsyncLocalStorage, modern izlemenin temel taşıdır. API ağ geçidinize gelen bir isteğe benzersiz bir traceId atanabilir. Bu ID daha sonra asenkron bağlamda saklanır ve alt hizmetlere yapılan herhangi bir giden API çağrısına (örneğin, bir HTTP başlığı olarak) otomatik olarak dahil edilir. Her hizmet aynı şeyi yapar ve bağlamı yayar. Merkezi loglama platformları daha sonra bu logları alabilir ve tüm sisteminizde bir isteğin uçtan uca akışını yeniden oluşturabilir.
2. Uluslararasılaştırma (i18n) ve Yerelleştirme (l10n)
Küresel bir uygulama için tarihleri, saatleri, sayıları ve para birimlerini bir kullanıcının yerel formatında sunmak kritiktir. Kullanıcının yerel ayarını (örneğin, 'fr-FR', 'ja-JP', 'en-US') istek başlıklarından veya kullanıcı profilinden alıp asenkron bağlama saklayabilirsiniz.
// Para birimini biçimlendirmek için bir yardımcı fonksiyon
import { requestContextStore } from './asyncContext.js';
function formatCurrency(amount, currencyCode) {
const context = requestContextStore.getStore();
const locale = context?.user?.locale || 'en-US'; // Bir varsayılana geri dön
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
}).format(amount);
}
// Uygulamanın derinliklerinde kullanım
const priceString = formatCurrency(199.99, 'EUR'); // Otomatik olarak kullanıcının yerel ayarını kullanır
Bu, locale değişkenini her yere geçirmek zorunda kalmadan tutarlı bir kullanıcı deneyimi sağlar.
3. Veritabanı İşlem (Transaction) Yönetimi
Tek bir isteğin birlikte başarılı olması veya başarısız olması gereken birden fazla veritabanı yazma işlemi yapması gerektiğinde, bir işleme (transaction) ihtiyacınız vardır. Bir istek işleyicisinin başında bir işlem başlatabilir, işlem istemcisini asenkron bağlamda saklayabilir ve ardından o istek içindeki tüm sonraki veritabanı çağrılarının otomatik olarak aynı işlem istemcisini kullanmasını sağlayabilirsiniz. İşleyicinin sonunda, sonuca göre işlemi onaylayabilir (commit) veya geri alabilirsiniz (rollback).
4. Özellik Bayrakları (Feature Toggling) ve A/B Testleri
Bir isteğin başında bir kullanıcının hangi özellik bayraklarına veya A/B test gruplarına ait olduğunu belirleyebilir ve bu bilgiyi bağlamda saklayabilirsiniz. Uygulamanızın farklı bölümleri, API katmanından render katmanına kadar, daha sonra bir özelliğin hangi sürümünü yürüteceğine veya hangi kullanıcı arayüzünü göstereceğine karar vermek için bağlama başvurabilir, bu da karmaşık parametre geçişi olmadan kişiselleştirilmiş bir deneyim yaratır.
Performans Değerlendirmeleri ve En İyi Uygulamalar
Yaygın bir soru şudur: performans yükü nedir? Node.js çekirdek ekibi, AsyncLocalStorage'ı son derece verimli hale getirmek için önemli çaba harcamıştır. C++ seviyesindeki async_hooks API'si üzerine inşa edilmiştir ve V8 JavaScript motoruyla derinden entegredir. Web uygulamalarının büyük çoğunluğu için performans etkisi ihmal edilebilir düzeydedir ve kod kalitesi ve sürdürülebilirlikteki büyük kazanımlarla fazlasıyla telafi edilir.
Etkili bir şekilde kullanmak için şu en iyi uygulamaları izleyin:
- Tekil Bir Örnek (Singleton Instance) Kullanın: Örneğimizde gösterildiği gibi, tutarlılık sağlamak için istek bağlamınız için tek, dışa aktarılmış bir
AsyncLocalStorageörneği oluşturun. - Giriş Noktasında Bağlamı Kurun:
als.run()'ı çağırmak için her zaman üst düzey bir ara yazılım veya bir istek işleyicisinin başlangıcını kullanın. Bu, bağlamınız için net ve öngörülebilir bir sınır oluşturur. - Depoyu Değişmez (Immutable) Olarak Kabul Edin: Depo nesnesinin kendisi değiştirilebilir olsa da, onu değişmez olarak kabul etmek iyi bir uygulamadır. İstek ortasında veri eklemeniz gerekirse, başka bir
run()çağrısıyla iç içe bir bağlam oluşturmak genellikle daha temizdir, ancak bu daha gelişmiş bir desendir. - Bağlam Olmayan Durumları Ele Alın: Logger'ımızda gösterildiği gibi, yardımcı programlarınız her zaman
getStore()'unundefineddöndürüp döndürmediğini kontrol etmelidir. Bu, arka plan betikleri veya uygulama başlangıcı gibi bir istek bağlamı dışında çalıştırıldıklarında zarif bir şekilde çalışmalarını sağlar. - Hata Yönetimi Sorunsuz Çalışır: Asenkron bağlam,
Promisezincirleri,.then()/.catch()/.finally()blokları vetry/catchileasync/awaitaracılığıyla doğru şekilde yayılır. Özel bir şey yapmanıza gerek yoktur; bir hata fırlatılırsa, bağlam hata yönetimi mantığınızda kullanılabilir durumda kalır.
Sonuç: Node.js Uygulamaları İçin Yeni Bir Çağ
AsyncLocalStorage, sadece kullanışlı bir yardımcı programdan daha fazlasıdır; sunucu taraflı JavaScript'te durum yönetimi için bir paradigma değişikliğini temsil eder. Yüksek derecede eşzamanlı bir ortamda istek kapsamlı bağlamı yönetme gibi uzun süredir devam eden bir soruna temiz, sağlam ve performanslı bir çözüm sunar.
Bu API'yi benimseyerek şunları yapabilirsiniz:
- Prop Drilling'i Ortadan Kaldırın: Daha temiz, daha odaklanmış fonksiyonlar yazın.
- Modüllerinizi Birbirinden Ayırın (Decouple): Bağımlılıkları azaltın ve kodunuzu yeniden düzenlemeyi ve test etmeyi kolaylaştırın.
- Gözlemlenebilirliği Artırın: Güçlü dağıtık izleme ve bağlamsal loglamayı kolaylıkla uygulayın.
- Gelişmiş Özellikler Oluşturun: İşlem yönetimi ve uluslararasılaştırma gibi karmaşık desenleri basitleştirin.
Node.js üzerinde modern, ölçeklenebilir ve küresel farkındalığa sahip uygulamalar oluşturan geliştiriciler için asenkron bağlama hakim olmak artık isteğe bağlı değil, temel bir beceridir. Eskimiş desenlerin ötesine geçerek ve AsyncLocalStorage'ı benimseyerek, sadece daha verimli değil, aynı zamanda derinlemesine daha zarif ve sürdürülebilir bir kod yazabilirsiniz.