Seriya: JavaScript Basics Dars 87

Streams API

Streams API nima, ReadableStream/WritableStream/TransformStream turlari, reader.read() orqali chunk-based o'qish, .text()/.json()/.blob() metodlarining ichki mexanizmi, TextDecoder, pipeThrough zanjiri, backpressure va pull model, hamda browser engine darajasida qanday ishlashi.

87-dars

Streams API

fetch()ni eslang:

const res = await fetch('/big-video.mp4');
const data = await res.blob(); // BUTUN fayl to'liq yuklanguncha kutadi

Bu yerda muammo: agar fayl 500MB bo'lsa, res.blob() hamma narsa to'liq kelmaguncha hech narsa qaytarmaydi. Xotirada butun fayl bir vaqtda saqlanishi kerak, va foydalanuvchi hech narsa ko'rmaydi, to hammasi tugamaguncha.

Streams API — bu muammoni yechadi: data'ni bir butun blok sifatida emas, balki kichik bo'laklar (chunk) oqimi sifatida qayta ishlash imkonini beradi — xuddi trubadan suv oqib turgani kabi, hammasi bir vaqtda kelishini kutmasdan, har bir tomchi kelgan zahoti ishlatish mumkin.


1. Uchta asosiy turi

Tur Vazifasi
ReadableStream Data manbai — undan o'qiysiz (masalan, network javobi, fayl)
WritableStream Data qabul qiluvchi — unga yozasiz (masalan, faylga saqlash)
TransformStream Ikkalasi bir vaqtda — kirgan data'ni o'zgartirib, chiqaradi (masalan, siqish, shifrlash)

2. ReadableStream — asosiy misol

const response = await fetch('/big-file.txt');
const reader = response.body.getReader(); // ReadableStream'dan reader olamiz

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  console.log('Chunk keldi:', value); // value — Uint8Array (bayt massivi)
}

Bu yerda response.body — bu butun javob emas, balki ReadableStream obyekti. fetch chaqirilganda, server javobi hali to'liq kelmagan bo'lishi mumkin — lekin siz allaqachon reader.read() orqali kelgan qismini darhol olishingiz mumkin, qolgani kelayotganda kutmasdan.

{ done, value } — har bir read() chaqiruvi shu formatda javob beradi:

  • done: false, value: Uint8Array(...) — hali data bor
  • done: true, value: undefined — stream tugadi

Nega bu FOYDALI — real misol: yuklab olish foizi

async function downloadWithProgress(url) {
  const response = await fetch(url);
  const total = Number(response.headers.get('content-length'));
  let loaded = 0;

  const reader = response.body.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    loaded += value.length;
    console.log(`Yuklandi: ${((loaded / total) * 100).toFixed(1)}%`);
  }

  return new Blob(chunks);
}

response.blob() bilan bu mumkin emas edi — chunki u sizga faqat oxirida, bitta yakuniy natija beradi. Streams orqali esa har bir chunk kelganda darhol reaksiya berish mumkin — progress bar, real-time preview va hokazo.


3. .text(), .json(), .blob() aslida nima qiladi

Bu yerda muhim tushuncha bor: fetch javobidagi .text(), .json(), .blob() metodlari — bu Streams API ustiga qurilgan qulaylik (convenience) metodlar:

// .text() aslida ICHKARIDA shunday qiladi:
async function myText(response) {
  const reader = response.body.getReader();
  let result = '';
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }
  return result;
}

Ya'ni .text() — bu shunchaki stream'ni to'liq o'qib, bitta string'ga yig'ib beruvchi yordamchi funksiya. Siz xom Streams API'ni ishlatganda, aynan shu ishni o'zingiz boshqarasiz — va shu orqali memory'ni tejash yoki progressiv ishlov berish imkoniga ega bo'lasiz.


4. Muhim: Uint8Array va TextDecoder

Stream orqali kelayotgan value — raw baytlar (Uint8Array), matn emas. Uni o'qish uchun TextDecoder kerak:

const decoder = new TextDecoder('utf-8');
const text = decoder.decode(value, { stream: true });

{ stream: true } muhim — chunki UTF-8'da bitta belgi (masalan, ⌘) bir nechta baytdan iborat bo'lishi mumkin, va agar chunk chegarasi aynan shu belgi o'rtasida kesib o'tsa, decoder bu holatni eslab qolib, keyingi chunk kelganda davom ettiradi — aks holda matn buzilib ketardi.


5. WritableStream — teskari yo'nalish

const writableStream = new WritableStream({
  write(chunk) {
    console.log('Yozildi:', chunk);
    // masalan, bu yerda faylga yoki serverga yuborish mumkin
  },
  close() {
    console.log('Stream yopildi');
  },
  abort(err) {
    console.error('Xato bilan to\'xtadi:', err);
  }
});

const writer = writableStream.getWriter();
writer.write('salom');
writer.write('dunyo');
writer.close();

Bu — teskari tomon: siz data'ni bo'lak-bo'lak yozasiz, va write(chunk) handler har safar avtomatik chaqiriladi.


6. TransformStream — ikkalasini bog'lash

const upperCaseTransform = new TransformStream({
  transform(chunk, controller) {
    const text = new TextDecoder().decode(chunk);
    controller.enqueue(new TextEncoder().encode(text.toUpperCase()));
  }
});

Bu — kirgan chunk'ni o'zgartirib, chiqishga yuboradi. Buning eng kuchli tomoni — pipeThrough orqali stream'larni zanjir qilib bog'lash:

const response = await fetch('/data.txt');

response.body
  .pipeThrough(new TextDecoderStream()) // baytlarni matnga aylantiradi
  .pipeThrough(upperCaseTransform)       // katta harfga o'giradi
  .pipeTo(someWritableStream);            // yakuniy manzilga yozadi

Bu — Unix pipe (cat file | grep x | sort) konsepsiyasiga to'g'ridan-to'g'ri o'xshaydi: har bir bosqich o'z vazifasini bajaradi, va data bir marta ham to'liq xotiraga yig'ilmasdan, oqim sifatida bir bosqichdan ikkinchisiga o'tadi.


7. Chuqurroq — Backpressure (eng muhim texnik tushuncha)

Bu — Streams API'ning eng chuqur va muhim g'oyasi. Muammo: agar production (data ishlab chiqaruvchi) tezroq ishlasa, consumptiondan (data iste'mol qiluvchidan) — masalan, tarmoqdan data juda tez kelayapti, lekin diskka yozish sekin — unda data xotirada to'planib ketadi (memory bloat, hatto crash).

Backpressure — bu iste'molchi "sekinlashtir, men hali tayyor emasman" deb ishlab chiqaruvchiga signal berish mexanizmi:

const writableStream = new WritableStream({
  write(chunk) {
    return new Promise(resolve => {
      // sekin operatsiya, masalan diskka yozish
      setTimeout(() => {
        console.log('Yozildi:', chunk);
        resolve(); // faqat SHU yerda keyingi chunk so'raladi
      }, 1000);
    });
  }
});

write() Promise qaytarganda, stream mexanizmi avtomatik ravishda keyingi chunk'ni yubormaydi, to bu Promise resolve bo'lguncha. Bu — butun zanjir bo'ylab (pipeThrough, pipeTo) avtomatik tarqaladi: agar oxirgi WritableStream sekin bo'lsa, bu signal orqaga, TransformStream orqali, hatto original ReadableStream'gacha (masalan, TCP darajasida — server yuborishni sekinlashtiradi) yetib boradi.

Bu — aynan TCP protokolining o'zidagi flow control g'oyasi bilan bir xil: browser darajasida ham, xuddi shunday, "iste'molchi band — kutib tur" signali ishlaydi.


8. V8 va browser engine darajasida nima sodir bo'ladi

ReadableStream, WritableStream — bular native, C++ darajasida (V8'ning o'zida emas, balki brauzer engine'ning umumiy qismida — Chromium'da third_party/blink/renderer/core/streams) amalga oshirilgan, lekin spetsifikatsiyasi to'liq JS-semantikasida yozilgan (WHATWG Streams Standard — bu spetsifikatsiya hujjat aslida pseudocode JS ko'rinishida yozilgan, noyob holat).

Network layer (C++) → data chunk keladi
    → Bu chunk browser'ning ICHKI buferiga qo'yiladi
    → ReadableStream controller.enqueue() chaqiradi (bu C++dan V8'ga signal)
    → V8 ichida bu — Promise resolve bilan teng: reader.read() kutayotgan
      Promise shu yerda bajariladi
    → Sizning JS kodingiz (onmessage o'rniga, await orqali) davom etadi

Muhim farq — Web Worker/WebSocket'dagi event-based modeldan farqli, Streams API to'liq Promise-based, pull modelda ishlaydi: siz o'zingiz reader.read() chaqirib, "menga keyingi chunk kerak" deb so'raysiz (pull), server/network esa faqat siz so'raganda javob beradi — bu ham backpressure'ning tabiiy natijasi (WebSocket'dagi kabi "push" emas).


9. Real hayotdagi qo'llanilishi

  • Katta fayllarni yuklab olish/yuklash progress bilan
  • Video/audio streaming (YouTube kabi — butun video kutilmaydi)
  • Katta CSV/JSON'ni bo'lak-bo'lak parse qilish
  • TextEncoderStream/TextDecoderStream orqali real-time matn kodlash
  • Service Worker'da katta javoblarni kesh bilan bir vaqtda foydalanuvchiga uzatish (response.body.pipeThrough)