Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. Oqim nima?
- 2.1 Butun javobni kutish
- 2.2 response.body — oqim
- 3. Reader bilan o'qish
- 3.1 getReader() va read()
- 3.2 Baytdan matnga: TextDecoder
- 3.3 Oqim bir marta o'qiladi
- 4. TextDecoderStream va pipeThrough
- 4.1 Oqimni o'zgartiruvchidan o'tkazish
- 4.2 for await qayerda ishlaydi?
- 5. Yuklash progressi
- 5.1 Content-Length + sanoq
- 5.2 Jonli progress chizig'i
- 5.3 Content-Length bo'lmasa
- 6. NDJSON: qatorma-qator JSON
- 6.1 Format
- 6.2 Uzilgan qator va bufer
- 7. O'z TransformStream ingiz
- 7.1 Qatorlarga bo'luvchi
- 7.2 Jonli "AI javobi"
- 8. Oqimni to'xtatish
- 9. Ko'p uchraydigan xatolar
- 9.1 Har bo'lakni JSON.parse qilish
- 9.2 { stream: true } siz decode
- 9.3 Tanani ikki marta o'qish
- 9.4 Content-Length ga ko'r-ko'rona ishonish
- 9.5 javob.ok ni tekshirmaslik
- 10. Mashqlar
- 1-mashq (oson): Bo'laklar va baytlar
- 2-mashq (o'rta): yuklabOl funksiyasi
- 3-mashq (qiyin): ndjsonOqi — async generator
- 11. Real ishda
- Xulosa
- Manbalar
Streams API: fetch javobini oqim bilan bo'lakma-bo'lak o'qish
Qisqacha:
await javob.text()butun javob kelishini kutadi. Oqim (stream) bilan esa har bo'lakni kelishi bilan ishlatasiz:javob.body.getReader()danread()har safar{ done, value }qaytaradi,value— baytlar. Baytni matngaTextDecoderStreamaylantiradi, qatorlarga esa o'zingiz yozganTransformStreambo'ladi. Shu bilan yuklash progressini ko'rsatasiz va AI javobi kabi "so'zma-so'z" chiqadigan matnni o'qiysiz.
Bu darsda
response.bodynima ekanini va unigetReader()bilan bo'lakma-bo'lak o'qishni bilasiz.- Baytlarni matnga to'g'ri aylantirasiz:
TextDecoderdagi{ stream: true }vaTextDecoderStream. Content-Lengthbilan yuklash progressini (foizini) hisoblaysiz.- NDJSON formatini o'qiysiz va bo'lak o'rtasida uzilgan qatorni bufer bilan yig'asiz.
- O'z
TransformStreamingizni yozasiz va oqimni o'rtasida to'xtatasiz.
Oldin bilishingiz kerak: Request, Response, Headers va fetch opsiyalari, Asinxron iteratsiya, Bekor qilish: AbortController, Typed arrays va binar ma'lumot.
1. Nega bu kerak?
«Bahor»da to'y. Rustam aka 50 likopcha osh suzyapti. Otabek ikki xil ishlashi mumkin:
- Hamma 50 likopcha tayyor bo'lishini kutadi va keyin birdaniga olib chiqadi. Birinchi mehmon 25 daqiqa och o'tiradi.
- Har likopcha tayyor bo'lishi bilan olib chiqadi. Birinchi mehmon 30 soniyada ovqatlana boshlaydi.
Ikkalasida ham oxirgi likopcha bir vaqtda yetadi. Farq — mehmon qachon nimadir ko'radi.
fetch asoslari dan beri biz birinchi usulda ishladik: await javob.json() butun javob kelishini kutadi. Kichik JSON uchun bu to'g'ri. Lekin uch holatda yetmaydi:
- Katta fayl. 200 MB video yuklanyapti. Foydalanuvchi bir daqiqa bo'sh ekranga qaraydi va "qotib qoldi" deb sahifani yopadi. Unga "43%" ko'rsatish kerak.
- Asta keladigan javob. ChatGPT yoki Claude javobni so'zma-so'z chiqaradi. Server javobni bo'laklab yuboradi, sahifa esa har bo'lakni darhol chizadi.
- Ko'p yozuvli javob. Server 10 000 ta buyurtmani birin-ketin yuboradi. Hammasini kutish o'rniga birinchi yozuvni darhol ko'rsatish mumkin.
Bugun javobni ikkinchi usulda — likopchama-likopcha — o'qishni o'rganamiz. Buning uchun brauzer va Node'da bitta umumiy vosita bor: Streams API.
2. Oqim nima?
2.1 Butun javobni kutish
Mashq API'da maxsus manzil bor: /api/mashq/oqim. U javobni bo'laklab yuboradi. bolaklar=4 — to'rt bo'lak, oraliq=300 — har bo'lak orasida 300 ms. Avval uni odatdagidek o'qiymiz.
Oldingi darslardan bir farq: bu yerda kodni async funksiyaga o'ramaymiz. await to'g'ridan-to'g'ri yozilgan — bu top-level await. Sayt bunday blokni modul sifatida ishga tushiradi, kompyuterda esa uni .mjs faylga saqlang:
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=4&oraliq=300";
const javob = await fetch(url);
console.log("Holat:", javob.status);
const matn = await javob.text();
console.log(matn.trim());Konsolda:
Holat: 200
1-bo'lak
2-bo'lak
3-bo'lak
4-bo'lak"Ishga tushir" ni bosing va qatorlar qachon chiqishiga qarang. Holat: 200 deyarli darhol chiqadi. To'rt bo'lak esa taxminan bir soniyadan keyin, birdaniga chiqadi.
Nega? fetch asoslari darsini eslang: fetch sarlavhalar (header'lar) kelishi bilan bajariladi. Tana (body) esa hali yo'lda bo'lishi mumkin. text() tananing oxirgi baytigacha kutadi. Server birinchi bo'lakni 300 ms oldin yuborgan bo'lsa ham, biz uni ko'rmaymiz.
2.2 response.body — oqim
Javobning tanasi aslida javob.body xususiyatida turadi. U — oqim (stream): ma'lumot bir martada emas, bo'laklar (chunk) bilan keladigan obyekt. Xuddi suv quvuri kabi: chelakni to'ldirib olib kelish o'rniga, kran ochiladi va suv oqib kelaveradi.
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=2&oraliq=100";
const javob = await fetch(url);
console.log(javob.body);Konsolda:
ReadableStream { locked: false, state: 'readable', supportsBYOB: true }ReadableStream — o'qiladigan oqim. Uning uch xususiyati ko'rindi: locked: false — hali hech kim o'qimayapti, state: 'readable' — o'qisa bo'ladi, supportsBYOB — maxsus tezkor o'qish rejimi (bizga kerak emas). Brauzer konsoli qisqaroq ko'rsatadi — ReadableStream {locked: false} — ma'nosi bir xil.
text(), json(), blob() — aslida shu oqimni oxirigacha o'qib, bir bo'lakka yig'adigan tayyor yordamchilar. Bugun oqimni o'zimiz o'qiymiz.
Streams API'da uch xil oqim bor. Sxemada ular qanday ulanishini ko'ring:
flowchart LR
S["Server<br/>bo'laklaydi"] --> R["ReadableStream<br/>javob.body"]
R --> T["TransformStream<br/>bayt → matn"]
T --> T2["TransformStream<br/>matn → qatorlar"]
T2 --> K["Sizning kodingiz<br/>read() / for await"]
K --> W["WritableStream<br/>fayl, ekran"]- ReadableStream — ma'lumot manbai: undan o'qiladi (
javob.body). - TransformStream — yo'ldagi o'zgartiruvchi: bir uchiga bo'lak kiradi, ikkinchisidan o'zgargani chiqadi.
- WritableStream — ma'lumot oxiri: unga yoziladi. Brauzerda kam ishlatiladi. Node'da fayl yozish uchun ko'p — Stream nima darsida, 24-qismda.
Bugun asosan birinchi ikkitasi bilan ishlaymiz.
Tekshirib ko'ring:
const javob = await fetch(url)qatori tugaganda server javobining hammasi kelgan bo'ladimi?
Javob
Yo'q, kafolat yo'q. fetch holat kodi va sarlavhalar kelishi bilan bajariladi. Tana hali kelayotgan bo'lishi mumkin — u javob.body oqimida. Butun tanani kutadigan — text(), json() kabi metodlar.
3. Reader bilan o'qish
3.1 getReader() va read()
Oqimni o'qish uchun unga o'quvchi (reader) ulanadi. Uni javob.body.getReader() qaytaradi. O'quvchining asosiy metodi — read(). U Promise qaytaradi va bitta bo'lak kelishini kutadi:
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=3&oraliq=300";
const javob = await fetch(url);
const oquvchi = javob.body.getReader();
while (true) {
const { done, value } = await oquvchi.read();
if (done) {
console.log("Oqim tugadi");
break;
}
console.log(value.constructor.name, value.length, "bayt");
}Konsolda:
Uint8Array 9 bayt
Uint8Array 9 bayt
Uint8Array 9 bayt
Oqim tugadiQatorma-qator:
read()har safar obyekt qaytaradi:{ done, value }. Uni destructuring bilan ikkiga ajratdik.done: false— bo'lak keldi, uvalueda.done: true— oqim tugadi,valueendiundefined.while (true)— cheksiz sikl. Undan chiqishning yagona yo'li —donekelgandabreak.value— matn emas,Uint8Array: baytlar massivi. Typed arrays va binar ma'lumot darsidan tanish. Tarmoq orqali har doim baytlar keladi.
Endi "Ishga tushir" ni bosing: qatorlar 300 ms oraliq bilan, birin-ketin chiqadi. Biz har bo'lakni kelishi bilan oldik.
Bu tanish naqsh. Asinxron iteratsiya darsida async iterator ham { value, done } qaytarardi. Oqim — vaqt o'tib keladigan ma'lumot, xuddi o'sha g'oya.
3.2 Baytdan matnga: TextDecoder
Baytlarni matnga TextDecoder aylantiradi. U standart bo'yicha UTF-8 kodlashni tushunadi: new TextDecoder().decode(baytlar).
Lekin oqimda bir tuzoq bor. UTF-8 da lotin harflari 1 bayt, — (tire) esa 3 bayt. Server bo'lakni xohlagan joyda kesishi mumkin — hatto bitta belgining o'rtasidan. Buni o'zimiz yasab ko'ramiz:
const baytlar = new TextEncoder().encode("Osh — 35 000");
console.log(baytlar.length); // 14
// Tire 4, 5 va 6-baytlarda. 5-baytdan kesamiz:
const birinchi = baytlar.slice(0, 5);
const ikkinchi = baytlar.slice(5);
const oddiy = new TextDecoder();
console.log(oddiy.decode(birinchi) + "|" + oddiy.decode(ikkinchi));
const oqimli = new TextDecoder();
const a = oqimli.decode(birinchi, { stream: true });
const b = oqimli.decode(ikkinchi, { stream: true });
console.log(a + "|" + b);Konsolda:
14
Osh �|�� 35 000
Osh |— 35 000TextEncoder — teskari ish: matnni baytlarga aylantiradi. "Osh — 35 000" da 12 belgi bor, lekin 14 bayt: tire uch bayt oldi.
Oddiy decode har bo'lakni alohida tushunishga urindi. Tirening uch baytidan biri birinchi bo'lakda, ikkitasi ikkinchisida qoldi. Hech biri yolg'iz o'zi belgi emas — shuning uchun � (almashtirish belgisi) chiqdi.
{ stream: true } esa decoder'ga "davomi bor" deydi. U chala baytni eslab qoladi va keyingi bo'lak bilan birlashtiradi. Qoida: oqimni bitta TextDecoder bilan, { stream: true } qo'shib o'qing.
3.3 Oqim bir marta o'qiladi
O'quvchi ulanganda oqim qulflanadi (locked): boshqa hech kim uni o'qiy olmaydi. O'qib bo'lingan oqim esa bo'sh. Shuning uchun tanani ikki marta o'qib bo'lmaydi:
const url = "https://ilmhamroh.uz/api/mashq/menyu";
const javob = await fetch(url);
await javob.json();
try {
await javob.json();
} catch (xato) {
console.log(`${xato.name}: ${xato.message}`);
}
const javob2 = await fetch(url);
javob2.body.getReader();
try {
await javob2.text();
} catch (xato) {
console.log(`${xato.name}: ${xato.message}`);
}Konsolda:
TypeError: Body is unusable: Body has already been read
TypeError: Body is unusable: Body has already been readTarjimasi: "Tanadan foydalanib bo'lmaydi: tana allaqachon o'qilgan." Chrome o'z so'zlari bilan aytadi, ikki holatni farqlab:
TypeError: Failed to execute 'json' on 'Response': body stream already read
TypeError: Failed to execute 'text' on 'Response': body stream is lockedBirinchisi — "oqim allaqachon o'qilgan", ikkinchisi — "oqim qulflangan" (unga o'quvchi ulangan). Tanani ikki joyda kerak bo'lsa, o'qishdan oldin javob.clone() qiling — Request, Response, Headers darsidagi kabi.
Tekshirib ko'ring: Server "Manti — 30 000" matnini ikki bo'lakda yubordi va kesish tirening o'rtasiga to'g'ri keldi. Har bo'lakka yangi
new TextDecoder()yaratib,{ stream: true }bilan o'qisangiz, tire to'g'ri chiqadimi?
Javob
Yo'q. Chala baytlarni decoder o'z ichida eslab qoladi. Har bo'lakka yangi decoder bo'lsa, birinchisining xotirasi ikkinchisiga o'tmaydi. Butun oqim uchun bitta decoder kerak.
4. TextDecoderStream va pipeThrough
4.1 Oqimni o'zgartiruvchidan o'tkazish
Har bo'lakni qo'lda decode qilish o'rniga, oqimni tayyor o'zgartiruvchidan o'tkazish mumkin. TextDecoderStream — baytlarni kiritib, matn chiqaradigan TransformStream. Ichida xuddi { stream: true } li bitta decoder ishlaydi.
Ulash metodi — pipeThrough(transform): "oqimni shu o'zgartiruvchidan o'tkaz va chiqqan oqimni qaytar". Natija — yangi ReadableStream, endi baytlar emas, satrlar oqimi:
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=3&oraliq=300";
const javob = await fetch(url);
const matnOqimi = javob.body.pipeThrough(new TextDecoderStream());
for await (const bolak of matnOqimi) {
console.log(JSON.stringify(bolak));
}Konsolda:
"1-bo'lak\n"
"2-bo'lak\n"
"3-bo'lak\n"Ikki yangilik bor. Birinchisi — pipeThrough. Ikkinchisi — o'quvchi va while o'rniga for await...of. Asinxron iteratsiya darsida u async iterable'larni aylanardi. ReadableStream ham async iterable — for await ichkarida o'quvchini o'zi oladi va done bo'lganda o'zi to'xtaydi.
JSON.stringify ni bo'lak qanday ko'rinishini aniq ko'rish uchun qo'ydik: har bo'lak oxirida \n — yangi qator belgisi bor.
4.2 for await qayerda ishlaydi?
Bitta ehtiyot chorasi. ReadableStream'ni for await bilan aylanish Chrome 124 va Firefox 110 da bor edi, Safari'ga esa faqat 27-versiyada (2026-yil sentabr) keldi. Ya'ni eski iPhone'larda for await (... of javob.body) xato beradi.
Ommaviy saytda ikki yo'l bor: getReader() + while (hamma joyda ishlaydi) yoki sahifa boshida tekshirish. Node 24 da for await bemalol ishlaydi. Bu darsdagi Node misollari shuning uchun for await bilan, brauzer misollari esa getReader() bilan yozilgan.
Maslahat: Brauzer funksiyasi qachondan va qaysi brauzerlarda borligini MDN sahifasining pastidagi "Browser compatibility" jadvalidan yoki caniuse.com saytidan tekshiring.
5. Yuklash progressi
5.1 Content-Length + sanoq
Foiz hisoblash uchun ikki son kerak: jami nechta bayt keladi va hozirgacha nechtasi keldi.
- Jami —
Content-Lengthsarlavhasida. Server uni javob boshida yuboradi (agar biladigan bo'lsa). - Keldi — har bo'lakning
value.lengthini qo'shib boramiz.
Mashq API'dagi /fayl — hajm baytlik "fayl", 10 bo'lakda keladi. 200 000 bayt yuklaymiz va har 25% ni yozamiz:
const url = "https://ilmhamroh.uz/api/mashq/fayl?hajm=200000";
const javob = await fetch(url);
const jami = Number(javob.headers.get("content-length"));
console.log("Jami:", jami, "bayt");
const oquvchi = javob.body.getReader();
let keldi = 0;
let oxirgiQadam = 0;
while (true) {
const { done, value } = await oquvchi.read();
if (done) break;
keldi += value.length;
const foiz = Math.floor((keldi / jami) * 100);
const qadam = Math.floor(foiz / 25) * 25;
if (qadam > oxirgiQadam) {
console.log(`${qadam}%`);
oxirgiQadam = qadam;
}
}
console.log("Keldi:", keldi, "bayt");Konsolda:
Jami: 200000 bayt
25%
50%
75%
100%
Keldi: 200000 baytNega har bo'lakda emas, faqat 25% qadamlarda chiqardik? Bo'laklar hajmi tarmoqqa bog'liq. Server 20 000 baytdan yuborsa ham, Node ularni 4 096 baytlik bo'laklarda oldi — 50 marta read(). Chrome'da esa shu fayl 11 bo'lakda keldi. Bo'lak soniga tayanmang — faqat jami baytlar soni aniq.
5.2 Jonli progress chizig'i
Endi buni ekranda ko'ramiz. Sahifadagi <progress> elementi — HTML'ning tayyor progress chizig'i (Guruhlash va natija elementlari darsida ko'rgansiz). Natija ochilishi bilan yuklash boshlanadi, tugmani bosib qaytarish mumkin:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
progress { width: 100%; height: 1.5rem; }
</style>
<button id="yukla" type="button">Qayta yuklash</button>
<p><progress id="chiziq" max="100" value="0"></progress></p>
<p id="holat" aria-live="polite">Kutilmoqda...</p>
<script>
const url = "https://ilmhamroh.uz/api/mashq/fayl?hajm=400000";
const chiziq = document.getElementById("chiziq");
const holat = document.getElementById("holat");
async function yukla() {
const javob = await fetch(url);
const jami = Number(javob.headers.get("content-length"));
const oquvchi = javob.body.getReader();
let keldi = 0;
while (true) {
const { done, value } = await oquvchi.read();
if (done) break;
keldi += value.length;
const foiz = Math.floor((keldi / jami) * 100);
chiziq.value = foiz;
holat.textContent = `${foiz}% — ${keldi} / ${jami} bayt`;
}
console.log("Yuklandi:", keldi, "bayt");
}
document.getElementById("yukla").addEventListener("click", yukla);
yukla();
</script>Konsolda:
Yuklandi: 400000 baytChiziq taxminan bir soniya ichida to'ladi — server har bo'lak orasida 100 ms kutadi. aria-live="polite" — ekran o'quvchi (masalan, NVDA bilan Malika) o'zgargan matnni o'qib beradi (Jonli hududlar).
5.3 Content-Length bo'lmasa
Uch holatda progressni bu usul bilan hisoblab bo'lmaydi:
- Sarlavha yo'q. Server javob hajmini oldindan bilmasa (masalan,
/oqimkabi bo'laklab yaratilsa),Content-Lengthyubormaydi.headers.get()nullqaytaradi,Number(null)esa0. FoizInfinitybo'ladi. Bunday holatda foiz o'rniga "1,2 MB yuklandi" deb ko'rsating. - Siqilgan javob. Server javobni gzip bilan siqib yuborsa (
Content-Encoding: gzip),Content-Length— siqilgan hajm.read()esa ochilgan baytlarni beradi. Sanoq 100% dan oshib ketadi. - CORS. Boshqa saytdan kelgan javobda
Content-Lengthni o'qish mumkin — u CORS "xavfsiz ro'yxati"da. Lekin boshqa maxsus sarlavhalarni serverAccess-Control-Expose-Headersbilan ochishi kerak (CORS mijoz tomondan).
Yuborish (upload) progressi esa boshqa mavzu. fetch bilan uni hali hamma brauzerda o'lchab bo'lmaydi — u Fayl yuklash va progress darsida, eski XMLHttpRequest bilan.
Tekshirib ko'ring: «
Content-Length+ sanoq» bo'limidagi koddaif (qadam > oxirgiQadam)sharti bo'lmasa, nima o'zgaradi?
Javob
Har bo'lakdan keyin 25%, 25%, 25% ... kabi bir xil qadam qayta-qayta chiqadi: 50 bo'lakda 50 qator. Shart har qadamni faqat bir marta, u birinchi marta yetilganda yozadi.
6. NDJSON: qatorma-qator JSON
6.1 Format
Server ko'p yozuvni oqim bilan yubormoqchi. Oddiy JSON massiv ([{...}, {...}]) bunga mos emas: JSON.parse faqat oxirgi ] kelganda ishlaydi.
NDJSON (Newline Delimited JSON) — "yangi qator bilan ajratilgan JSON". Har qatorda bitta to'liq JSON obyekt:
{"n":1,"matn":"1-bo'lak"}
{"n":2,"matn":"2-bo'lak"}
{"n":3,"matn":"3-bo'lak"}Har qator o'zi alohida JSON.parse qilinadi. Qator kelishi bilan uni ishlatish mumkin. Sarlavhasi odatda application/x-ndjson. Bu format katta eksportlarda, loglarda va lokal AI modellari (masalan, Ollama) javobida uchraydi. JSON Lines (.jsonl) — aynan shu formatning boshqa nomi.
6.2 Uzilgan qator va bufer
Lekin tarmoq qatorni o'rtasidan kesishi mumkin. Bo'lak {"n":1}\n{"n" bo'lib kelsa, ikkinchi qism hali to'liq JSON emas.
Yechim — bufer: hali tugamagan qator bo'lagini saqlab turadigan o'zgaruvchi. Har yangi bo'lakda:
- Bo'lakni buferga qo'shamiz.
- Buferni
\nbo'yicha bo'lamiz. - Oxirgi qism — tugallanmagan bo'lishi mumkin. Uni buferga qaytaramiz.
- Qolganlari — to'liq qatorlar. Ularni
JSON.parseqilamiz.
Qadamma-qadam ko'ring. Tarmoq o'rniga qo'lda yasalgan uch bo'lak — ataylab noqulay joylardan kesilgan:
Asosiy hiyla — pop(). U massivning oxirgi elementini olib tashlaydi va qaytaradi. Bo'lak \n bilan tugasa, oxirgi element bo'sh satr bo'ladi — bufer tozalanadi. Tugamasa — chala qator buferda keyingi bo'lakni kutadi.
Tekshirib ko'ring: Bufersiz yozsangiz — har bo'lakni to'g'ridan-to'g'ri
split("\n")qilib, har qisminiJSON.parseqilsangiz — vizualdagi birinchi bo'lakda nima bo'ladi?
Javob
{"n":1} muvaffaqiyatli o'qiladi, lekin {"n" qismi JSON.parse da xato beradi — SyntaxError. Bufer bo'lmasa, tarmoq kesgan har qator dasturni yiqitadi.
7. O'z TransformStream ingiz
7.1 Qatorlarga bo'luvchi
Bufer mantiqini har safar while ichida yozish noqulay. Uni TransformStream ga o'rab, TextDecoderStream kabi pipeThrough bilan ulash mumkin.
new TransformStream({ transform, flush }) ikki funksiya oladi:
transform(bolak, boshqaruvchi)— har kirgan bo'lak uchun chaqiriladi. Chiqishga narsa yuborish —boshqaruvchi.enqueue(qiymat)(enqueue— inglizcha "navbatga qo'yish": qiymat chiqish oqimida o'quvchini kutib turadi).flush(boshqaruvchi)— kirish oqimi tugaganda bir marta chaqiriladi. Buferda qolgan oxirgi qatorni chiqarish uchun kerak.
function qatorlargaBol() {
let bufer = "";
return new TransformStream({
transform(bolak, boshqaruvchi) {
bufer += bolak;
const qatorlar = bufer.split("\n");
bufer = qatorlar.pop();
for (const qator of qatorlar) {
if (qator.trim() !== "") boshqaruvchi.enqueue(qator);
}
},
flush(boshqaruvchi) {
if (bufer.trim() !== "") boshqaruvchi.enqueue(bufer);
},
});
}
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=3&format=ndjson";
const javob = await fetch(url);
const qatorlar = javob.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(qatorlargaBol());
for await (const qator of qatorlar) {
const yozuv = JSON.parse(qator);
console.log(yozuv.n, yozuv.matn);
}Konsolda:
1 1-bo'lak
2 2-bo'lak
3 3-bo'lakqatorlargaBol — fabrika funksiya (Closure amaliyotda): har chaqiriqda o'z bufer i bor yangi TransformStream yasaydi. bufer closure'da yashaydi. transform va flush — obyekt ichidagi qisqa metod yozuvi.
Ikki pipeThrough zanjir bo'lib ulandi: baytlar → matn → qatorlar. Har bo'g'in bitta ish qiladi — mermaid sxemasidagi quvur aynan shu.
if (qator.trim() !== "") — bo'sh qatorlarni o'tkazib yuboradi. Ba'zi serverlar qatorlar orasiga bo'sh qator qo'yadi, JSON.parse("") esa xato beradi.
7.2 Jonli "AI javobi"
Brauzerda oqimning eng ko'zga ko'rinadigan ishlatilishi — matnni kelishi bilan chizish. Quyidagi natija NDJSON oqimini o'qiydi va har yozuvni ekranga qo'shadi. Safari'ning eski versiyalarida ham ishlashi uchun for await o'rniga o'quvchi bilan:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#javob { min-height: 3rem; padding: 0.5rem; }
#javob { border: 1px solid #c9d6cf; }
</style>
<button id="sora" type="button">Qayta so'rash</button>
<p id="javob" aria-live="polite"></p>
<script>
const url = "https://ilmhamroh.uz/api/mashq/oqim"
+ "?bolaklar=6&oraliq=250&format=ndjson";
const javobEl = document.getElementById("javob");
async function sora() {
javobEl.textContent = "";
const javob = await fetch(url);
const oquvchi = javob.body
.pipeThrough(new TextDecoderStream())
.getReader();
let bufer = "";
let soni = 0;
while (true) {
const { done, value } = await oquvchi.read();
if (done) break;
bufer += value;
const qatorlar = bufer.split("\n");
bufer = qatorlar.pop();
for (const qator of qatorlar) {
const yozuv = JSON.parse(qator);
javobEl.textContent += yozuv.matn + " ";
soni++;
}
}
console.log("Yozuvlar:", soni);
}
document.getElementById("sora").addEventListener("click", sora);
sora();
</script>Konsolda:
Yozuvlar: 6So'zlar 250 ms oraliq bilan birin-ketin paydo bo'ladi. ChatGPT va Claude'ning veb-sahifalari ham shunday ishlaydi. Faqat ular formati boshqa — Server-Sent Events. Uni WebSocket va Server-Sent Events darsida o'qiymiz.
getReader() ni pipeThrough dan keyin chaqirdik. Shuning uchun value — baytlar emas, tayyor matn.
8. Oqimni to'xtatish
Ba'zan oqimning hammasi kerak emas: foydalanuvchi "To'xtat" tugmasini bosdi yoki kerakli yozuv topildi. Ikki yo'l bor.
oquvchi.cancel() — "boshqa kerak emas". Oqim yopiladi, keyingi read() darhol { done: true } qaytaradi. Brauzer ulanishni uzadi va server ham yuborishni to'xtatadi. Mashq API shunday qiladi: mijoz uzilganini ko'rib, qolgan bo'laklarni yozmaydi.
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=5&oraliq=200";
const javob = await fetch(url);
const oquvchi = javob.body
.pipeThrough(new TextDecoderStream())
.getReader();
for (let i = 0; i < 2; i++) {
const { value } = await oquvchi.read();
console.log(value.trim());
}
await oquvchi.cancel();
console.log(await oquvchi.read());Konsolda:
1-bo'lak
2-bo'lak
{ value: undefined, done: true }Beshta bo'lakdan ikkitasini oldik va to'xtatdik. Qolgan uchtasi uchun kutilmadi — dastur darhol tugadi.
AbortController — Bekor qilish: AbortController darsidagi usul. fetch(url, { signal }) ga berilgan signal oqim o'qilayotganda ham ishlaydi: abort() chaqirilsa, navbatdagi read() AbortError bilan rad etiladi. Bu yo'l qulay, chunki bitta signal ham fetch ni, ham o'qishni to'xtatadi. Masalan, AbortSignal.timeout(10_000) — "10 soniyada tugamasa, to'xtat".
Tekshirib ko'ring: Oqim har 300 ms da bo'lak yuboradi,
fetchga esasignal: AbortSignal.timeout(500)berilgan. Nechta bo'lak o'qiladi va keyingiread()qanday xato nomi bilan rad etiladi?
Javob
Ikkita: birinchisi darhol, ikkinchisi 300 ms da keladi. 500 ms da vaqt tugaydi va keyingi read() TimeoutError bilan rad etiladi — AbortError emas. Node 24 da shunday sinadik. abort() ni o'zingiz chaqirsangiz, nomi AbortError bo'ladi — xuddi fetch opsiyalari darsidagidek.
9. Ko'p uchraydigan xatolar
9.1 Har bo'lakni JSON.parse qilish
const matnOqimi = javob.body.pipeThrough(new TextDecoderStream());
for await (const bolak of matnOqimi) {
const yozuv = JSON.parse(bolak); // ❌ bo'lak — qator emas
}Lokal kompyuterda ishlaydi, chunki bo'laklar kichik va qatorlar butun keladi. Serverda, sekin internetda qator o'rtasidan kesiladi va SyntaxError chiqadi. Tuzatish: avval bufer bilan qatorlarga bo'ling (qatorlargaBol), keyin parse qiling.
9.2 { stream: true } siz decode
Lotin matnda xato ko'rinmaydi: hamma harf bir bayt. Birinchi tire, ʻ, kirill harfi yoki emoji kelganda � paydo bo'ladi — va faqat ba'zan, tarmoq qayerdan kesganiga qarab. Bunday "ba'zan" xatoni topish juda qiyin. Tuzatish: TextDecoderStream ishlating yoki bitta decoder'ni { stream: true } bilan.
9.3 Tanani ikki marta o'qish
console.log(await javob.text()) bilan tekshirib, keyin javob.json() qilish — Body is unusable / body stream already read. Tuzatish: tanani bir marta o'qing va o'zgaruvchida saqlang, yoki oldindan javob.clone().
9.4 Content-Length ga ko'r-ko'rona ishonish
null bo'lsa — foiz Infinity, siqilgan bo'lsa — 100% dan oshadi. Tuzatish: jami 0 yoki noma'lum bo'lsa, foiz o'rniga yuklangan hajmni ko'rsating; foizni Math.min(100, ...) bilan cheklang.
9.5 javob.ok ni tekshirmaslik
Oqim bilan o'qiganda ham 404 yoki 500 javobning tanasi oqim bo'lib keladi — xato JSON'i yoki HTML sahifa. Uni "fayl" deb saqlab qo'yish mumkin. Tuzatish: o'qishdan oldin if (!javob.ok) — xuddi fetch asoslari dagi kabi.
10. Mashqlar
1-mashq (oson): Bo'laklar va baytlar
/api/mashq/oqim?bolaklar=5&oraliq=100 dan javobni getReader() bilan o'qing. Oxirida nechta bo'lak va jami nechta bayt kelganini chiqaring: 5 bo'lak, 45 bayt. Har bo'lak "1-bo'lak\n" — 9 bayt.
Yechim
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=5&oraliq=100";
const javob = await fetch(url);
const oquvchi = javob.body.getReader();
let soni = 0;
let baytlar = 0;
while (true) {
const { done, value } = await oquvchi.read();
if (done) break;
soni++;
baytlar += value.length;
}
console.log(`${soni} bo'lak, ${baytlar} bayt`); // 5 bo'lak, 45 baytBo'laklar soni bu yerda 5 chiqdi, chunki server ularni 100 ms oraliq bilan yuboradi va har biri alohida keladi. Sekin tarmoqda ikkitasi bitta bo'lakka qo'shilib kelishi mumkin — baytlar soni esa har doim 45.
2-mashq (o'rta): yuklabOl funksiyasi
yuklabOl(url, foizdaChaqir) funksiyasini yozing. U javobni oqim bilan o'qisin, har bo'lakdan keyin foizdaChaqir(foiz) ni chaqirsin va oxirida barcha baytlarni bitta Uint8Array qilib qaytarsin. javob.ok bo'lmasa — Error tashlasin. Content-Length yo'q bo'lsa — foizdaChaqir ni chaqirmasin.
Sinov: ?hajm=50000 faylni yuklang, foizlardan faqat 100 ni chiqaring, keyin qaytgan massiv uzunligi va birinchi 10 baytini matn qilib chiqaring.
Ishora: bo'laklarni massivga yig'ing, oxirida new Uint8Array(jamiUzunlik) yarating va har bo'lakni .set(bolak, joy) bilan joyiga ko'chiring (Typed arrays va binar ma'lumot).
Yechim
async function yuklabOl(url, foizdaChaqir) {
const javob = await fetch(url);
if (!javob.ok) {
throw new Error(`Yuklab bo'lmadi: ${javob.status}`);
}
const jami = Number(javob.headers.get("content-length"));
const oquvchi = javob.body.getReader();
const bolaklar = [];
let keldi = 0;
while (true) {
const { done, value } = await oquvchi.read();
if (done) break;
bolaklar.push(value);
keldi += value.length;
if (jami > 0) {
foizdaChaqir(Math.min(100, Math.floor((keldi / jami) * 100)));
}
}
const natija = new Uint8Array(keldi);
let joy = 0;
for (const bolak of bolaklar) {
natija.set(bolak, joy);
joy += bolak.length;
}
return natija;
}
const fayl = await yuklabOl(
"https://ilmhamroh.uz/api/mashq/fayl?hajm=50000",
(foiz) => {
if (foiz === 100) console.log("100%");
},
);
console.log(fayl.length);
console.log(new TextDecoder().decode(fayl.slice(0, 10)));Konsolda:
100%
50000
abcdefghijnatija.set(bolak, joy) — bolak ni natija ga joy indeksidan boshlab ko'chiradi. /fayl ning mazmuni — takrorlanuvchi abc...xyz0123456789 qatorlari, shuning uchun birinchi 10 bayt abcdefghij.
3-mashq (qiyin): ndjsonOqi — async generator
ndjsonOqi(url) nomli async generator yozing (Asinxron iteratsiya): u NDJSON javobni oqim bilan o'qisin va har qatorni JSON.parse qilib yield qilsin. Shunda uni bunday ishlatish mumkin bo'ladi:
for await (const yozuv of ndjsonOqi(url)) {
console.log(yozuv.n, yozuv.matn);
}Talablar: bufer bilan uzilgan qatorlarni yig'ing; bo'sh qatorlarni o'tkazib yuboring; oqim tugaganda buferda qolganini ham chiqaring. Sinov: /api/mashq/oqim?bolaklar=4&format=ndjson — lekin uchinchi yozuvdan keyin break qiling. Faylni kurs/mashqlar/11/21-oqimlar/ndjson.mjs ga saqlang.
Yechim
async function* ndjsonOqi(url) {
const javob = await fetch(url);
if (!javob.ok) {
throw new Error(`So'rov muvaffaqiyatsiz: ${javob.status}`);
}
const matnOqimi = javob.body.pipeThrough(new TextDecoderStream());
let bufer = "";
for await (const bolak of matnOqimi) {
bufer += bolak;
const qatorlar = bufer.split("\n");
bufer = qatorlar.pop();
for (const qator of qatorlar) {
if (qator.trim() !== "") yield JSON.parse(qator);
}
}
if (bufer.trim() !== "") yield JSON.parse(bufer);
}
const url =
"https://ilmhamroh.uz/api/mashq/oqim?bolaklar=4&format=ndjson";
for await (const yozuv of ndjsonOqi(url)) {
console.log(yozuv.n, yozuv.matn);
if (yozuv.n === 3) break;
}
console.log("To'xtadik");Konsolda:
1 1-bo'lak
2 2-bo'lak
3 3-bo'lak
To'xtadikbreak bu yerda kuchli ishlaydi. Tashqi for await to'xtaganda generator'ning ichki for await i ham yopiladi. U esa oqimni cancel() qiladi — to'rtinchi bo'lak kutilmaydi. Async generator va oqim bir-biriga juda mos keladi: ikkalasi ham "vaqt o'tib keladigan qiymatlar".
git add 11/21-oqimlar/ndjson.mjs
git commit -m "11/21: NDJSON oqimini async generator bilan o'qish"11. Real ishda
- AI chat interfeyslari. OpenAI, Anthropic, Google Gemini API'lari javobni oqim bilan beradi; ularning rasmiy SDK'lari ichida xuddi shu
getReader()+ bufer + qatorlarga bo'lish bor. O'zbek startaplari AI yordamchi yasasa, birinchi qiladigan ishi — oqimni ekranga chizish. - Fayl yuklash ekranlari. Google Drive, Telegram Web — yuklab olish progressi
Content-Length+ sanoq bilan. - Katta eksportlar. "Hamma buyurtmalarni CSV/NDJSON qilib yuklab olish" — server oqim bilan yuboradi, xotira to'lmaydi. Node tomonini Async iteratorlar va Web Streams darsida ko'rasiz.
- Siqish. Brauzerda
CompressionStream("gzip")ham TransformStream — oqimni yo'lda siqadi. Ulash usuli bugungidek:pipeThrough. - Intervyu. "Fetch javobini qanday qilib progress bilan o'qiysiz?", "
res.json()ni ikki marta chaqirsa nima bo'ladi?" — middle frontend intervyularida uchraydi.
Xulosa
fetchsarlavhalar bilan bajariladi; tanajavob.bodyda — ReadableStream.text()/json()uni oxirigacha o'qib yig'adi.getReader().read()→{ done, value };value—Uint8Arraybaytlar. Oqim bir marta o'qiladi va o'quvchi uni qulflaydi.- Baytdan matnga —
TextDecoderStreamyoki bittaTextDecoder+{ stream: true }; aks holda ko'p baytli belgilar buziladi. - Progress:
Content-Length/ yig'ilganvalue.length. Sarlavha yo'q yoki siqilgan bo'lishi mumkin — tekshiring. - NDJSON — har qatorda bitta JSON; uzilgan qatorni bufer yig'adi. Mantiqni
TransformStreamga o'rab,pipeThroughbilan ulang. oquvchi.cancel()yokiAbortController— oqimni o'rtasida to'xtatadi.
Keyingi dars: API klient qatlami — ilovadagi hamma fetch larni bitta joyga yig'amiz: asosiy URL, xatolarni bir xil shaklga keltirish, timeout va vazifalar uchun api.js.
Manbalar
- WHATWG Streams Standard — streams.spec.whatwg.org
- MDN: "Using readable streams", "TextDecoderStream", "TransformStream", "Response.body" — developer.mozilla.org
- web-features 3.40: "Async iterable streams" (Baseline 2026-09, Safari 27), "Streams", "Encoding" — github.com/web-platform-dx/web-features
- NDJSON spesifikatsiyasi — github.com/ndjson/ndjson-spec; JSON Lines — jsonlines.org
- Node.js 24 hujjatlari: "Web Streams API" — nodejs.org/api/webstreams.html
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!