Pembahasan mendalam tentang Helper Async Iterator JavaScript 'scan', menjelajahi fungsionalitas, kasus penggunaan, dan manfaatnya untuk pemrosesan akumulatif asinkron.
Helper Async Iterator JavaScript: Scan - Pemrosesan Akumulatif Asinkron
Pemrograman asinkron adalah landasan pengembangan JavaScript modern, terutama ketika berhadapan dengan operasi yang terikat I/O, seperti permintaan jaringan atau interaksi sistem file. Async iterator, yang diperkenalkan di ES2018, menyediakan mekanisme yang kuat untuk menangani aliran data asinkron. Helper `scan`, yang sering ditemukan di pustaka seperti RxJS dan semakin tersedia sebagai utilitas mandiri, membuka lebih banyak potensi untuk memproses aliran data asinkron ini.
Memahami Async Iterator
Sebelum mendalami `scan`, mari kita ulas kembali apa itu async iterator. Async iterator adalah objek yang sesuai dengan protokol async iterator. Protokol ini mendefinisikan metode `next()` yang mengembalikan promise yang me-resolve ke objek dengan dua properti: `value` (nilai berikutnya dalam urutan) dan `done` (boolean yang menunjukkan apakah iterator telah selesai). Async iterator sangat berguna saat bekerja dengan data yang tiba seiring waktu, atau data yang memerlukan operasi asinkron untuk diambil.
Berikut adalah contoh dasar dari async iterator:
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
async function main() {
const iterator = generateNumbers();
let result = await iterator.next();
console.log(result); // { value: 1, done: false }
result = await iterator.next();
console.log(result); // { value: 2, done: false }
result = await iterator.next();
console.log(result); // { value: 3, done: false }
result = await iterator.next();
console.log(result); // { value: undefined, done: true }
}
main();
Memperkenalkan Helper `scan`
Helper `scan` (juga dikenal sebagai `accumulate` atau `reduce`) mengubah async iterator dengan menerapkan fungsi akumulator ke setiap nilai dan memancarkan hasil yang terakumulasi. Ini analog dengan metode `reduce` pada array, tetapi beroperasi secara asinkron dan pada iterator.
Pada intinya, `scan` mengambil async iterator, fungsi akumulator, dan nilai awal opsional. Untuk setiap nilai yang dipancarkan oleh iterator sumber, fungsi akumulator dipanggil dengan nilai terakumulasi sebelumnya (atau nilai awal jika ini adalah iterasi pertama) dan nilai saat ini dari iterator. Hasil dari fungsi akumulator menjadi nilai terakumulasi berikutnya, yang kemudian dipancarkan oleh async iterator yang dihasilkan.
Sintaks dan Parameter
Sintaks umum untuk menggunakan `scan` adalah sebagai berikut:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator`: Async iterator yang akan diubah.
- `accumulator`: Fungsi yang menerima dua argumen: nilai terakumulasi sebelumnya dan nilai saat ini dari iterator. Fungsi ini harus mengembalikan nilai terakumulasi yang baru.
- `initialValue` (opsional): Nilai awal untuk akumulator. Jika tidak disediakan, nilai pertama dari iterator sumber akan digunakan sebagai nilai awal, dan fungsi akumulator akan dipanggil mulai dari nilai kedua.
Kasus Penggunaan dan Contoh
Helper `scan` sangat serbaguna dan dapat digunakan dalam berbagai skenario yang melibatkan aliran data asinkron. Berikut adalah beberapa contoh:
1. Menghitung Total Berjalan
Bayangkan Anda memiliki async iterator yang memancarkan jumlah transaksi. Anda dapat menggunakan `scan` untuk menghitung total berjalan dari transaksi-transaksi ini.
async function* generateTransactions() {
yield 10;
yield 20;
yield 30;
}
async function main() {
const transactions = generateTransactions();
const runningTotals = scan(transactions, (acc, value) => acc + value, 0);
for await (const total of runningTotals) {
console.log(total); // Keluaran: 10, 30, 60
}
}
main();
Dalam contoh ini, fungsi `accumulator` hanya menambahkan jumlah transaksi saat ini ke total sebelumnya. `initialValue` sebesar 0 memastikan bahwa total berjalan dimulai dari nol.
2. Mengakumulasi Data ke dalam Array
Anda dapat menggunakan `scan` untuk mengakumulasi data dari async iterator ke dalam sebuah array. Ini bisa berguna untuk mengumpulkan data dari waktu ke waktu dan memprosesnya dalam batch.
async function* fetchData() {
yield { id: 1, name: 'Alice' };
yield { id: 2, name: 'Bob' };
yield { id: 3, name: 'Charlie' };
}
async function main() {
const dataStream = fetchData();
const accumulatedData = scan(dataStream, (acc, value) => [...acc, value], []);
for await (const data of accumulatedData) {
console.log(data); // Keluaran: [{id: 1, name: 'Alice'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
}
}
main();
Di sini, fungsi `accumulator` menggunakan spread operator (`...`) untuk membuat array baru yang berisi semua elemen sebelumnya dan nilai saat ini. `initialValue`-nya adalah array kosong.
3. Menerapkan Rate Limiter
Kasus penggunaan yang lebih kompleks adalah menerapkan rate limiter. Anda dapat menggunakan `scan` untuk melacak jumlah permintaan yang dibuat dalam jendela waktu tertentu dan menunda permintaan berikutnya jika batas laju terlampaui.
async function* generateRequests() {
// Mensimulasikan permintaan yang masuk
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 200));
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
yield Date.now();
}
async function main() {
const requests = generateRequests();
const rateLimitWindow = 1000; // 1 detik
const maxRequestsPerWindow = 2;
async function* rateLimitedRequests(source, window, maxRequests) {
let queue = [];
for await (const requestTime of source) {
queue.push(requestTime);
queue = queue.filter(t => requestTime - t < window);
if (queue.length > maxRequests) {
const earliestRequest = queue[0];
const delay = window - (requestTime - earliestRequest);
console.log(`Batas laju terlampaui. Menunda selama ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
yield requestTime;
}
}
const limited = rateLimitedRequests(requests, rateLimitWindow, maxRequestsPerWindow);
for await (const requestTime of limited) {
console.log(`Permintaan diproses pada ${requestTime}`);
}
}
main();
Contoh ini menggunakan `scan` secara internal (dalam fungsi `rateLimitedRequests`) untuk memelihara antrean stempel waktu permintaan. Ia memeriksa apakah jumlah permintaan dalam jendela batas laju melebihi maksimum yang diizinkan. Jika ya, ia menghitung penundaan yang diperlukan dan berhenti sejenak sebelum menghasilkan permintaan.
4. Membangun Agregator Data Real-time (Contoh Global)
Bayangkan sebuah aplikasi keuangan global yang perlu mengagregasi harga saham real-time dari berbagai bursa. Async iterator dapat mengalirkan pembaruan harga dari bursa seperti New York Stock Exchange (NYSE), London Stock Exchange (LSE), dan Tokyo Stock Exchange (TSE). `scan` dapat digunakan untuk menjaga harga rata-rata berjalan atau harga tertinggi/terendah untuk saham tertentu di semua bursa.
// Mensimulasikan streaming harga saham dari berbagai bursa
async function* generateStockPrices() {
yield { exchange: 'NYSE', symbol: 'AAPL', price: 170.50 };
yield { exchange: 'LSE', symbol: 'AAPL', price: 170.75 };
await new Promise(resolve => setTimeout(resolve, 50));
yield { exchange: 'TSE', symbol: 'AAPL', price: 170.60 };
}
async function main() {
const stockPrices = generateStockPrices();
// Gunakan scan untuk menghitung harga rata-rata berjalan
const runningAverages = scan(
stockPrices,
(acc, priceUpdate) => {
const { total, count } = acc;
return { total: total + priceUpdate.price, count: count + 1 };
},
{ total: 0, count: 0 }
);
for await (const averageData of runningAverages) {
const averagePrice = averageData.total / averageData.count;
console.log(`Harga rata-rata berjalan: ${averagePrice.toFixed(2)}`);
}
}
main();
Dalam contoh ini, fungsi `accumulator` menghitung total berjalan dari harga dan jumlah pembaruan yang diterima. Harga rata-rata akhir kemudian dihitung dari nilai-nilai yang terakumulasi ini. Ini memberikan pandangan real-time tentang harga saham di berbagai pasar global.
5. Menganalisis Lalu Lintas Situs Web Secara Global
Bayangkan sebuah platform analitik web global yang menerima aliran data kunjungan situs web dari server yang berlokasi di seluruh dunia. Setiap titik data mewakili pengguna yang mengunjungi situs web. Menggunakan `scan`, kita dapat menganalisis tren tayangan halaman per negara secara real time. Katakanlah datanya terlihat seperti: `{ country: "US", page: "homepage", timestamp: 1678886400 }`.
async function* generateWebsiteVisits() {
yield { country: 'US', page: 'homepage', timestamp: Date.now() };
yield { country: 'CA', page: 'product', timestamp: Date.now() };
yield { country: 'UK', page: 'blog', timestamp: Date.now() };
yield { country: 'US', page: 'product', timestamp: Date.now() };
}
async function main() {
const visitStream = generateWebsiteVisits();
const pageViewCounts = scan(
visitStream,
(acc, visit) => {
const { country } = visit;
const newAcc = { ...acc };
newAcc[country] = (newAcc[country] || 0) + 1;
return newAcc;
},
{}
);
for await (const counts of pageViewCounts) {
console.log('Jumlah tayangan halaman berdasarkan negara:', counts);
}
}
main();
Di sini, fungsi `accumulator` memperbarui penghitung untuk setiap negara. Output akan menunjukkan akumulasi jumlah tayangan halaman untuk setiap negara seiring data kunjungan baru tiba.
Manfaat Menggunakan `scan`
Helper `scan` menawarkan beberapa keuntungan saat bekerja dengan aliran data asinkron:
- Gaya Deklaratif: `scan` memungkinkan Anda untuk mengekspresikan logika pemrosesan akumulatif dengan cara yang deklaratif dan ringkas, meningkatkan keterbacaan dan pemeliharaan kode.
- Penanganan Asinkron: Ini menangani operasi asinkron dalam fungsi akumulator dengan mulus, membuatnya cocok untuk skenario kompleks yang melibatkan tugas-tugas terikat I/O.
- Pemrosesan Real-time: `scan` memungkinkan pemrosesan aliran data secara real-time, memungkinkan Anda bereaksi terhadap perubahan saat terjadi.
- Dapat Disusun (Composability): Ini dapat dengan mudah disusun dengan helper async iterator lainnya untuk membuat pipeline pemrosesan data yang kompleks.
Mengimplementasikan `scan` (Jika Tidak Tersedia)
Meskipun beberapa pustaka menyediakan helper `scan` bawaan, Anda dapat dengan mudah mengimplementasikannya sendiri jika diperlukan. Berikut adalah implementasi sederhana:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
let first = true;
for await (const value of sourceIterator) {
if (first && initialValue === undefined) {
accumulatedValue = value;
first = false;
} else {
accumulatedValue = accumulator(accumulatedValue, value);
}
yield accumulatedValue;
}
}
Implementasi ini melakukan iterasi pada iterator sumber dan menerapkan fungsi akumulator ke setiap nilai, menghasilkan hasil yang terakumulasi. Ini menangani kasus di mana tidak ada `initialValue` yang disediakan dengan menggunakan nilai pertama dari iterator sumber sebagai nilai awal.
Perbandingan dengan `reduce`
Penting untuk membedakan `scan` dari `reduce`. Meskipun keduanya beroperasi pada iterator dan menggunakan fungsi akumulator, keduanya berbeda dalam perilaku dan outputnya.
- `scan` memancarkan nilai terakumulasi untuk setiap iterasi, memberikan riwayat berjalan dari akumulasi.
- `reduce` hanya memancarkan nilai terakumulasi akhir setelah memproses semua elemen dalam iterator.
Oleh karena itu, `scan` cocok untuk skenario di mana Anda perlu melacak status antara dari akumulasi, sementara `reduce` cocok ketika Anda hanya membutuhkan hasil akhir.
Penanganan Kesalahan (Error Handling)
Saat bekerja dengan async iterator dan `scan`, sangat penting untuk menangani kesalahan dengan baik. Kesalahan dapat terjadi selama proses iterasi atau di dalam fungsi akumulator. Anda dapat menggunakan blok `try...catch` untuk menangkap dan menangani kesalahan ini.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Terjadi kesalahan!');
yield 3;
}
async function main() {
const dataStream = generatePotentiallyFailingData();
try {
const accumulatedData = scan(dataStream, (acc, value) => acc + value, 0);
for await (const data of accumulatedData) {
console.log(data);
}
} catch (error) {
console.error('Terjadi kesalahan:', error);
}
}
main();
Dalam contoh ini, blok `try...catch` menangkap kesalahan yang dilemparkan oleh iterator `generatePotentiallyFailingData`. Anda kemudian dapat menangani kesalahan tersebut dengan tepat, seperti mencatatnya atau mencoba kembali operasi tersebut.
Kesimpulan
Helper `scan` adalah alat yang ampuh untuk melakukan pemrosesan akumulatif asinkron pada async iterator JavaScript. Ini memungkinkan Anda untuk mengekspresikan transformasi data yang kompleks dengan cara yang deklaratif dan ringkas, menangani operasi asinkron dengan baik, dan memproses aliran data secara real-time. Dengan memahami fungsionalitas dan kasus penggunaannya, Anda dapat memanfaatkan `scan` untuk membangun aplikasi asinkron yang lebih kuat dan efisien. Baik Anda menghitung total berjalan, mengakumulasi data ke dalam array, menerapkan rate limiter, atau membangun agregator data real-time, `scan` dapat menyederhanakan kode Anda dan meningkatkan kinerjanya secara keseluruhan. Ingatlah untuk mempertimbangkan penanganan kesalahan dan memilih `scan` daripada `reduce` ketika Anda memerlukan akses ke nilai akumulasi antara selama pemrosesan aliran data asinkron Anda. Menjelajahi pustaka seperti RxJS dapat lebih meningkatkan pemahaman dan aplikasi praktis Anda tentang `scan` dalam paradigma pemrograman reaktif.