Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Bulk: bir so'rovda ko'p yozuv
- 2.1 Ikki strategiya
- 2.2 Atomik: PUT /v1/vazifalar
- 2.3 Qisman: POST /v1/vazifalar/toplam va 207
- 2.4 Uch amaliy qoida
- 3. Uzoq davom etuvchi operatsiya: 202 Accepted
- 3.1 Naqsh
- 3.2 Node'da ishlaydigan namuna
- 3.3 Ish xatosi yutib yuborilmaydi
- 4. Polling yoki webhook
- 4.1 Taqqoslash
- 4.2 Yaxshi polling qoidalari
- 5. Natija faylini berish
- 6. API qadami: to'plam, 207 va eksport (holat 27, 1-qism)
- 6.1 Marshrutlar
- 6.2 Eksport moduli
- 6.3 Natija (kanon curl, sarlavhalar qisqartirilgan)
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): 207 dan qayta yuborish ro'yxati
- 3-mashq (qiyin): chegarali polling
- 4-mashq: API qadami
- 9. Real ishda
- Xulosa
- Manbalar
Bulk va uzoq davom etuvchi operatsiyalar: 207 Multi-Status, 202 Accepted va polling
Qisqacha: Ko'p yozuvni bitta so'rovda yuborish (bulk) ikki xil bo'ladi. Atomik — hammasi yoki hech narsa: bitta element xato bo'lsa 422 va hech narsa yozilmaydi. Qisman muvaffaqiyat — har element o'z natijasini oladi va javob 207 Multi-Status bo'ladi. Uzoq ish (eksport, katta import, hisobot) so'rov ichida bajarilmaydi: server darhol 202 Accepted +
Location: /v1/eksportlar/<id>qaytaradi, ish fonda ketadi, mijoz holat resursini so'rab turadi (polling) yoki server unga xabar beradi (webhook). Tayyor bo'lganda holat javobida fayl havolasi paydo bo'ladi.
Bu darsda
- Bulk so'rovni atomik yoki qisman muvaffaqiyatli qilish o'rtasida to'g'ri tanlaysiz.
- 207 javobini har element uchun status bilan yasaysiz va mijozda o'qiysiz.
- 202 Accepted + holat resursi +
Retry-Afterbilan uzoq ish naqshini quryapsiz. - Fondagi ish event loop'ni bloklamasligi uchun bo'laklarga bo'lasiz.
- Polling va webhook farqini, eksport faylini qanday berishni bilasiz.
Oldin bilishingiz kerak: Pagination: offset, cursor, keyset, Status kodlarni to'g'ri tanlash, Event loop'ni bloklash, HTTP caching va shartli so'rovlar.
1. Nega bu kerak?
Ikki vaziyat.
Birinchisi. Frontend'ga "Import" tugmasi qo'shmoqchisiz: foydalanuvchi JSON fayldan 300 ta vazifa yuklaydi. Har vazifaga alohida POST — 300 ta so'rov, 300 ta ulanish va javob. Telefonda bu sekin. Yo'lda internet uzilsa — 147 tasi yozilgan, qolgani yo'q, va qaysilari yozilgani noma'lum.
Ikkinchisi. Malika "Hamma vazifalarni CSV qilib yuklab olish" tugmasini bosdi. Vazifalar 200 000 ta. Server ularni o'qib, CSV qilib, yuborguncha 90 soniya ketadi. Bu vaqtda brauzer kutadi. nginx esa sukut bo'yicha 60 s dan keyin (proxy_read_timeout) kutishni to'xtatadi va 504 qaytaradi (server timeout darsi). Foydalanuvchi sahifani yangilaydi va ishni yana boshidan boshlatadi.
O'xshatish: kimyoviy tozalash (himchistka). Kiyimingizni topshirasiz va darhol chek olasiz — "3 kundan keyin keling". Kassada 3 kun kutib turmaysiz. Keyin chek raqami bilan "tayyormi?" deb so'raysiz (polling) yoki ular sizga SMS yuboradi (webhook). Chek — 202 javobidagi Location.
2. Bulk: bir so'rovda ko'p yozuv
2.1 Ikki strategiya
| Atomik ("hammasi yoki hech narsa") | Qisman muvaffaqiyat | |
|---|---|---|
| bitta element xato | 422, hech narsa yozilmaydi | yaroqlilari yoziladi, xatolar ro'yxatda |
| javob | 200/201 yoki 422 | 207 (aralash) yoki 201 (hammasi yaxshi) |
| qachon | import, sinxronlash — yarim holat ma'nosiz | log, telemetriya, ko'p mustaqil buyurtmalar |
| qayta urinish | oddiy — butunlay qayta yuboriladi | murakkab — faqat xatolilarni |
Kanon ikkalasini ham qildi — ikki xil endpoint bilan.
2.2 Atomik: PUT /v1/vazifalar
22-qism frontend'ining sinxronlash mantiqi butun ro'yxatni bir martada yuboradi: api.almashtir(yangi). Bu — to'plamni almashtirish: PUT resursga (bu yerda — to'plamga) to'liq yangi holatni yozadi (metodlar darsi). Shuning uchun u idempotent: ikki marta yuborilsa ham natija bir xil.
PUT /v1/vazifalar
Content-Type: application/json
{ "vazifalar": [
{ "id": 5, "matn": "Import #uy", "bajarildi": false },
{ "matn": "Non, sut va \"qatiq\"", "bajarildi": true }
] }Qoidalar (kanon): yaroqli va takrorlanmas id saqlanadi, id siz yoki takrorlanganiga yangi id beriladi. Bitta element xato bo'lsa — 422, hamma xato bir javobda (body.vazifalar[1], body.vazifalar[2].id), ombor o'zgarmaydi. Javob — { versiya, vazifalar }.
versiya — to'plam versiyasi: har yozuvda +1. Oldingi darsda har vazifaning o'z versiyasi bor edi, endi butun ro'yxatniki ham. GET javobi ham { versiya, vazifalar, sahifa } bo'ldi — frontend'ning serverRoyxatiSxemasi aynan versiya ni majburiy kutadi.
2.3 Qisman: POST /v1/vazifalar/toplam va 207
Ikkinchi endpoint har elementni alohida yaratadi. 207 Multi-Status — "javobda bir nechta status bor, tanaga qarang". U WebDAV standartidan (RFC 4918) keladi va REST API'larda bulk uchun keng ishlatiladi. Quyidagi brauzer bloki 207 javobini yasaydi:
function validate(item) {
const text = typeof item?.matn === "string" ? item.matn.trim() : "";
if (text.length < 1 || text.length > 200) {
return "matn 1–200 belgi bo'lishi kerak";
}
return null;
}
function bulkCreate(items, nextId) {
const results = items.map((item, index) => {
const error = validate(item);
if (error) {
return {
index, status: 422,
error: { joy: `body.vazifalar[${index}]`, xabar: error },
};
}
return { index, status: 201, task: { id: nextId++, ...item } };
});
const allOk = results.every((r) => r.status === 201);
return { status: allOk ? 201 : 207, body: { results } };
}
const res = bulkCreate(
[{ matn: "A #ish" }, { matn: "" }, { matn: "B", bajarildi: true }],
7,
);
console.log(res.status);
for (const r of res.body.results) {
console.log(r.index, r.status, r.task?.id ?? r.error.xabar);
}Konsolda:
207
0 201 7
1 422 matn 1–200 belgi bo'lishi kerak
2 201 8index — muhim: mijoz natijani o'zi yuborgan elementga shu raqam bilan bog'laydi. Hammasi yaroqli bo'lsa — oddiy 201, chunki "aralash" holat yo'q.
Kanonning haqiqiy javobi (holat 27, qisqartirilgan):
HTTP/1.1 207 Multi-Status
{"natijalar":[{"indeks":0,"holat":201,"vazifa":{"id":7,"matn":"A #ish",…}},
{"indeks":1,"holat":422,"xato":{"joy":"body.vazifalar[1]","xabar":"matn 1–200 belgi bo'lishi kerak"}},
{"indeks":2,"holat":201,"vazifa":{"id":8,"matn":"B","bajarildi":true,…}}]}2.4 Uch amaliy qoida
- Hajm chegarasi. Kanonda
toplam— 1..100 element,PUTto'plam — 1000 gacha (ro'yxat chegarasi). Chegarasiz bulk — bitta so'rov bilan serverni band qilish yo'li. - Marshrut tartibi.
/toplammarshruti/:iddan oldin ro'yxatdan o'tadi. Aks holda Expresstoplamni:iddeb oladi (Router darsi). Kod bo'yicha natija:GET /toplam— 400 ("id — musbat butun son"),POST /toplamesa/:iddagi.all(metodYoq(...))ga tushib 405 oladi — bulk umuman ishlamaydi. - Tranzaksiya. Kanonda
toplamelementlari ketma-ket, tranzaksiyasiz yoziladi (xotira ombori). Postgres'da atomikPUTbitta tranzaksiya bo'ladi (tranzaksiya darsi).
Tekshirib ko'ring: 207 javobi kelganda mijoz
res.okni tekshirdi vatrueoldi. Bu yetarlimi?
Javob
Yo'q. 207 — 2xx oilasi, res.ok true. Lekin ichida 422 lar bo'lishi mumkin. Mijoz 207 ni ko'rganda har elementning holat ini tekshirishi va xatolilarni foydalanuvchiga ko'rsatishi (yoki qayta yuborishi) shart. 207 ni "hammasi yaxshi" deb o'qish — yarim ma'lumot jim yo'qolishiga olib keladi.
3. Uzoq davom etuvchi operatsiya: 202 Accepted
3.1 Naqsh
Uzoq davom etuvchi operatsiya (long-running operation, LRO) — so'rov-javob vaqtiga sig'maydigan ish. Naqsh uch qadamdan iborat:
sequenceDiagram
participant M as Mijoz
participant S as Server
participant F as Fon ishi
M->>S: POST /v1/eksportlar
S->>F: ishni boshla (kutmaydi)
S-->>M: 202, Location: /v1/eksportlar/1
loop Retry-After soniyada bir
M->>S: GET /v1/eksportlar/1
S-->>M: 200 {holat: bajarilmoqda, tayyor: 100}
end
F-->>S: tayyor
M->>S: GET /v1/eksportlar/1
S-->>M: 200 {holat: tayyor, fayl: …/fayl}
M->>S: GET /v1/eksportlar/1/fayl
S-->>M: 200 text/csvPOST— ish yaratiladi, server darhol 202 qaytaradi.Location— ishning holat resursi,Retry-After— qancha kutib so'rash.GET <Location>— holat:kutilmoqda,bajarilmoqda,tayyor,xato+ jarayon (tayyor: 100,jami: 250).- Tayyor bo'lganda holat javobida natija havolasi —
fayl.
202 Accepted ma'nosi: "so'rov qabul qilindi, lekin ish hali tugamagan, natija keyin". 201 dan farqi: 201 — resurs allaqachon yaratilgan.
3.2 Node'da ishlaydigan namuna
Blok 250 ta vazifani CSV'ga eksport qiladi. Mijoz 202 oladi, holatni so'rab turadi va faylni yuklab oladi. Blok Node uchun:
import { createServer } from "node:http";
import { setImmediate as yieldLoop } from "node:timers/promises";
const tasks = Array.from(
{ length: 250 }, (_, i) => `Vazifa ${i + 1}`,
);
const jobs = new Map();
let nextId = 1;
async function runExport(job) {
for (let i = 0; i < tasks.length; i += 100) {
await yieldLoop(); // event loop'ni bo'shatamiz
job.state = "running";
const chunk = tasks.slice(i, i + 100);
job.csv += chunk.map((t, k) => `${i + k + 1},${t}\r\n`).join("");
job.done += chunk.length;
}
job.state = "ready";
}
function view(job) {
const v = { id: job.id, state: job.state, done: job.done };
if (job.state === "ready") v.file = `/v1/eksportlar/${job.id}/fayl`;
return v;
}
const server = createServer((req, res) => {
const send = (status, body, headers = {}) => {
const all = { "content-type": "application/json", ...headers };
res.writeHead(status, all);
res.end(JSON.stringify(body));
};
if (req.method === "POST" && req.url === "/v1/eksportlar") {
const id = String(nextId++);
const job = { id, state: "queued", done: 0, csv: "" };
jobs.set(id, job);
void runExport(job); // kutmaymiz — ish fonda
return send(202, view(job), {
location: `/v1/eksportlar/${id}`, "retry-after": "1",
});
}
const m = /^\/v1\/eksportlar\/(\d+)(\/fayl)?$/.exec(req.url);
const job = m && jobs.get(m[1]);
if (!job) return send(404, { title: "Not Found" });
if (m[2]) {
res.writeHead(200, { "content-type": "text/csv" });
return res.end(job.csv);
}
const wait = job.state === "ready" ? {} : { "retry-after": "1" };
send(200, view(job), wait);
});
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
const base = `http://127.0.0.1:${server.address().port}`;
const start = await fetch(`${base}/v1/eksportlar`, {
method: "POST",
});
const where = start.headers.get("location");
console.log(start.status, where, await start.json());
let info;
do {
// namunada 20 ms — haqiqiy mijoz Retry-After ni kutadi
await new Promise((ok) => setTimeout(ok, 20));
info = await (await fetch(base + where)).json();
} while (info.state !== "ready");
console.log(info);
const csv = await (await fetch(base + info.file)).text();
const lines = csv.split("\r\n").filter(Boolean);
console.log(lines.length, "qator;", lines[0], "…", lines.at(-1));
server.closeAllConnections();
server.close();Konsolda:
202 /v1/eksportlar/1 { id: '1', state: 'queued', done: 0 }
{ id: '1', state: 'ready', done: 250, file: '/v1/eksportlar/1/fayl' }
250 qator; 1,Vazifa 1 … 250,Vazifa 250To'rt joyga qarang:
void runExport(job)—awaitsiz. Handler javobni darhol yuboradi, ish fonda davom etadi.void— "Promise'ni ataylab kutmayapman" degan belgi (lint uchun ham).await yieldLoop()— har 100 qatordan oldinsetImmediate. Ish bo'laklarga bo'linadi va orada boshqa so'rovlar ham xizmat oladi (event loop'ni bloklash). 50 000 qatorni bitta siklda yozsangiz, shu vaqt davomida server boshqa hech kimga javob bermaydi.- 202 javobida holat
queued— ish hali boshlanmagan, chunkirunExportbirinchi qadamdayoqawaitqildi va boshqaruvni qaytardi. - Mijoz 20 ms kutdi — namunada tez bo'lishi uchun. Haqiqiy mijoz
Retry-Afterdagi soniyani kutadi.
Testda polling uchun "tayyor bo'lguncha" sikl va qisqa kutish — normal. Faqat chegara qo'ying (masalan 50 urinish), aks holda xato bo'lsa test abadiy aylanadi.
3.3 Ish xatosi yutib yuborilmaydi
Fondagi ish xato bilan tugasa nima bo'ladi? Javob allaqachon ketgan — throw hech qayerga yetmaydi va ushlanmagan rad etilish jarayonni o'ldirishi mumkin. Kanon xatoni ish holatiga yozadi va logga chiqaradi:
bajar(ish).catch((x: unknown) => {
ish.holat = "xato";
ish.xato = "Eksport bajarilmadi";
console.error(`[eksport ${ish.id}]`, x);
});
res.status(202).location(`${req.baseUrl}/${ish.id}`)
.set("Retry-After", "1").json(korinish(ish, req.baseUrl));Mijoz polling paytida holat: "xato" ni ko'radi. Ichki tafsilot — faqat logda (Problem Details darsi qoidasi).
Tekshirib ko'ring: Nega eksport holati javobida
Cache-Control: no-store, vazifa javobida esaprivate, no-cache?
Javob
Eksport holati har soniyada o'zgaradi (tayyor: 100 → 200 → 250) va qisqa yashaydi — saqlab qo'yishdan foyda yo'q, eski nusxa esa mijozni chalg'itadi ("hali 100 da"). Vazifa esa kam o'zgaradi — saqlash va 304 bilan tekshirish foydali (kesh darsi).
4. Polling yoki webhook
4.1 Taqqoslash
| Polling | Webhook | |
|---|---|---|
| Kim boshlaydi | mijoz so'raydi: "tayyormi?" | server mijozga POST yuboradi: "tayyor" |
| Mijozga nima kerak | hech narsa — oddiy HTTP mijoz | ochiq HTTPS manzil (server) |
| Kechikish | Retry-After gacha |
deyarli darhol |
| Ortiqcha so'rovlar | ko'p ("yo'q, hali yo'q") | yo'q |
| Murakkablik | oddiy | imzo (HMAC), qayta yuborish, idempotentlik |
| Brauzer va mobil ilova uchun | mos | mos emas — ularda server yo'q |
Kanon polling'ni tanladi: mijoz — brauzerdagi frontend, uning webhook qabul qiluvchi manzili yo'q. Webhook — serverdan serverga aloqa uchun (chiquvchi webhook darsi); to'lov tizimlari (Click, Payme) natijani aynan webhook bilan yuboradi.
4.2 Yaxshi polling qoidalari
Retry-Afterni hurmat qiling — har 50 ms so'rash serverni bekorga yuklaydi.- Uzoq ishda kutishni oshiring: 1 s, 2 s, 4 s … (eksponensial kutish).
- Urinishlar soniga yoki umumiy vaqtga chegara qo'ying.
- Holat resursi tez javob bersin — u ishni o'zi bajarmaydi, faqat holatni o'qiydi.
5. Natija faylini berish
Tayyor eksport — fayl. Uni berishda uch narsa:
Content-Disposition: attachment; filename=…— brauzer faylni ko'rsatmay, yuklab oladi. Express'dares.attachment("vazifalar-2026-10-24.csv").- To'g'ri CSV — RFC 4180: maydonda vergul, qo'shtirnoq yoki yangi qator bo'lsa, u qo'shtirnoqqa olinadi va ichidagi
"ikkilanadi. Qator oxiri —\r\n. - Muddat — natija abadiy saqlanmaydi. Kanonda 1 soat, keyin holat resursi ham 404.
CSV maydonini qochirish (brauzerda ham ishlaydi):
function csvField(x) {
const s = String(x);
return /[",\r\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
}
function csvRow(fields) {
return fields.map(csvField).join(",") + "\r\n";
}
const row = csvRow([6, 'Non, sut va "qatiq"', true, ""]);
console.log(JSON.stringify(row));
console.log(csvRow(["oddiy", 42]).trim());Konsolda:
"6,\"Non, sut va \"\"qatiq\"\"\",true,\r\n"
oddiy,42JSON.stringify qatorni ko'rinmas belgilari bilan ko'rsatadi: vergulli matn qo'shtirnoqda, ichidagi " ikkilangan (""qatiq""), oxirida \r\n. Bu Excel va Google Sheets ochadigan aniq shakl.
Katta fayllar uchun real tizimlar natijani obyekt omboriga (S3, MinIO) yozadi va mijozga vaqtinchalik havola (presigned URL) beradi (S3 darsi). Kanon esa xotirada saqlaydi — 1 soat, bitta jarayon.
Diqqat: CSV'ni Excel'da ochadigan foydalanuvchilar uchun yana bir xavf bor —
=,+,-,@bilan boshlanadigan maydon formula sifatida bajarilishi mumkin (CSV injection). Masalan vazifa matni=HYPERLINK(...). Buning himoyasi — bunday maydon boshiga'qo'shish. Kanonda bu yo'q; ochiq API'da o'ylab ko'ring.
6. API qadami: to'plam, 207 va eksport (holat 27, 1-qism)
Bu qadam holat 26 dan boshlanadi. Commit — keyingi darsda, API.md bilan birga (holat 27). Hozircha commit qilmang.
6.1 Marshrutlar
src/vazifalar/marshrut.ts ga ikkita yangi narsa — PUT to'plam va /toplam:
r.route("/")
// ... GET, POST
// PUT to'plamga — butun ro'yxatni almashtirish;
// atomik: bitta xato bo'lsa hech narsa yozilmaydi
.put(jsonKerak, async (req, res) => {
const t = toplamniTekshir(req.body);
if (!t.ok) throw new HttpXato(422,
`${String(t.xatolar.length)} ta element noto'g'ri`,
{ kod: "validatsiya", xatolar: t.xatolar });
const { vazifalar, versiya } =
await xizmat.almashtirHammasi(t.qiymat);
keshYoq(res);
res.json({ versiya, vazifalar });
})
.all(metodYoq("GET, HEAD, POST, PUT"));
// /:id dan OLDIN — aks holda "toplam" id deb olinadi
r.route("/toplam")
.post(jsonKerak, async (req, res) => {
const tana: unknown = req.body;
const elementlar =
typeof tana === "object" && tana !== null
&& "vazifalar" in tana && Array.isArray(tana.vazifalar)
? (tana.vazifalar as unknown[]) : undefined;
if (elementlar === undefined || elementlar.length === 0
|| elementlar.length > 100) {
throw validatsiyaXatosi("body.vazifalar",
"1..100 elementli massiv");
}
const tekshirilgan =
elementlar.map((el) => yangiVazifaniTekshir(el));
const yaroqli =
tekshirilgan.flatMap((t) => (t.ok ? [t.qiymat] : []));
const yaratilgan = await xizmat.toplamYarat(yaroqli);
let k = 0;
const natijalar = tekshirilgan.map((t, indeks): {
indeks: number; holat: number; vazifa?: Vazifa;
xato?: MaydonXatosi;
} => {
if (!t.ok) {
return { indeks, holat: 422,
xato: { joy: `body.vazifalar[${String(indeks)}]`,
xabar: t.xato } };
}
const vazifa = yaratilgan[k++];
return vazifa === undefined
? { indeks, holat: 500 } : { indeks, holat: 201, vazifa };
});
const hammasiOk = natijalar.every((n) => n.holat === 201);
res.status(hammasiOk ? 201 : 207).json({ natijalar });
})
.all(metodYoq("POST"));Ombor interfeysiga almashtirHammasi() qo'shildi va RoyxatNatija endi versiya ni ham qaytaradi — 26-qismdagi Postgres ombori ham shuni amalga oshiradi.
GET ro'yxat javobiga ham to'plam versiyasi qo'shiladi:
res.json({
versiya: natija.versiya,
vazifalar: natija.vazifalar.map((v) => proyeksiya(v, maydonlar)),
sahifa,
});Qolgan o'zgarishlar (kanondan, holat 26 → 27). src/vazifalar/ombor.ts — interfeysga:
export type AlmashtirishVazifasi = {
id?: number; matn: string; bajarildi: boolean;
};
// RoyxatNatija ichiga:
/** butun to'plam versiyasi (har yozuvda +1) — frontend sinxroni
* (22-qism `versiya`) va ro'yxat ETag'i uchun */
versiya: number;
// VazifaOmbori ichiga:
/** Butun to'plamni almashtirish (PUT /v1/vazifalar): berilgan
* yaroqli takrorlanmas id saqlanadi, qolganiga yangi id. Atomik */
almashtirHammasi(vazifalar: readonly AlmashtirishVazifasi[]):
Promise<{ vazifalar: Vazifa[]; versiya: number }>;src/vazifalar/xotira-ombor.ts — #versiya = 0 maydoni; yarat, yangila, ochir oxirida this.#versiya++; royxat natijasiga versiya: this.#versiya; yangi metod:
almashtirHammasi(yangilar: readonly AlmashtirishVazifasi[]):
Promise<{ vazifalar: Vazifa[]; versiya: number }> {
const hozir = this.#soat().toISOString();
const band = new Set<number>();
for (const y of yangilar) {
if (y.id !== undefined && !band.has(y.id)) band.add(y.id);
}
let keyingi = Math.max(this.#keyingiId,
...[...band].map((id) => id + 1));
const ishlatildi = new Set<number>();
const eski = this.#vazifalar;
const yangi = new Map<number, Vazifa>();
for (const y of yangilar) {
const id = y.id !== undefined && !ishlatildi.has(y.id)
? y.id : keyingi++;
ishlatildi.add(id);
const avval = eski.get(id);
yangi.set(id, {
id,
matn: y.matn,
bajarildi: y.bajarildi,
teglar: teglariniOl(y.matn),
yaratildi: avval?.yaratildi ?? hozir,
yangilandi: hozir,
versiya: (avval?.versiya ?? 0) + 1,
});
}
eski.clear();
for (const [id, v] of yangi) eski.set(id, v);
this.#keyingiId = keyingi;
this.#versiya++;
return Promise.resolve({
vazifalar: structuredClone([...yangi.values()]),
versiya: this.#versiya,
});
}Atomiklik shu yerda: yangi Map to'liq yig'ilgandan keyingina eski ma'lumot almashtiriladi. Tekshiruv esa undan ham oldin — marshrutda.
src/vazifalar/xizmat.ts — ikki metod:
/** PUT /v1/vazifalar — butun to'plam (frontend "almashtir") */
almashtirHammasi: (vazifalar: readonly AlmashtirishVazifasi[]) =>
ombor.almashtirHammasi(vazifalar),
/** Bulk yaratish: yaroqlilari yaratiladi, qolganlari — xato bilan */
async toplamYarat(yangilar: readonly YangiVazifa[]):
Promise<Vazifa[]> {
if ((await ombor.soni({})) + yangilar.length > MAKS_VAZIFA) {
throw new HttpXato(409,
`Ro'yxat to'ladi (${String(MAKS_VAZIFA)})`,
{ kod: "royxat-toldi", sarlavha: "Vazifalar soni chegarada" });
}
const yaratilgan: Vazifa[] = [];
// Ketma-ket, tranzaksiyasiz: xotirada xato bo'lmaydi;
// Postgres'da (26-qism) bitta tranzaksiya
for (const y of yangilar) yaratilgan.push(await ombor.yarat(y));
return yaratilgan;
},src/vazifalar/vazifa.ts — to'plam tekshiruvi (qo'lda; validatsiya darslarida Zod'ga o'tadi):
export const MAKS_TOPLAM = 1000;
export type AlmashtirishElementi = {
id?: number; matn: string; bajarildi: boolean;
};
type Xatolar = { joy: string; xabar: string }[];
/** PUT /v1/vazifalar tanasi:
* { vazifalar: [{ id?, matn, bajarildi }] }
* — har element joyi bilan xato */
export function toplamniTekshir(tana: unknown):
| { ok: true; qiymat: AlmashtirishElementi[] }
| { ok: false; xatolar: Xatolar } {
if (!obyektmi(tana) || !Array.isArray(tana.vazifalar)) {
return { ok: false, xatolar: [
{ joy: "body.vazifalar", xabar: "massiv bo'lishi kerak" }] };
}
if (tana.vazifalar.length > MAKS_TOPLAM) {
return { ok: false, xatolar: [{ joy: "body.vazifalar",
xabar: `ko'pi bilan ${String(MAKS_TOPLAM)} ta` }] };
}
const xatolar: Xatolar = [];
const natija: AlmashtirishElementi[] = [];
for (const [i, el] of (tana.vazifalar as unknown[]).entries()) {
const joy = `body.vazifalar[${String(i)}]`;
if (!obyektmi(el)) {
xatolar.push({ joy, xabar: "obyekt bo'lishi kerak" });
continue;
}
const { id, ...qolgan } = el;
if (id !== undefined && !(typeof id === "number"
&& Number.isSafeInteger(id) && id > 0)) {
xatolar.push({ joy: `${joy}.id`, xabar: "musbat butun son" });
}
const t = toliqVazifaniTekshir(qolgan);
if (!t.ok) xatolar.push({ joy, xabar: t.xato });
else natija.push(typeof id === "number"
? { id, ...t.qiymat } : t.qiymat);
}
return xatolar.length > 0
? { ok: false, xatolar } : { ok: true, qiymat: natija };
}Kanonda Xatolar tur nomi yo'q — { joy: string; xabar: string }[] uch joyda to'g'ridan yozilgan; bu yerda qator qisqarsin deb nom berildi.
6.2 Eksport moduli
Yangi papka — src/eksport/. To'liq src/eksport/eksport.ts (kanondan; uzun qatorlar telefon uchun bo'lingan, ma'no aynan):
// eksport.ts — uzoq davom etuvchi operatsiya: POST → 202 Accepted
// + Location (holat resursi), mijoz so'rab turadi (polling), tayyor
// bo'lganda natija fayli havolasi. Ishlar xotirada (bitta jarayon);
// ko'p jarayon/qayta ishga tushish — navbat (BullMQ, 27/29-qism)
import { randomUUID } from "node:crypto";
import { setImmediate as navbat } from "node:timers/promises";
import { Router } from "express";
import { HttpXato } from "../umumiy/http-xato.ts";
import { csvQator } from "../umumiy/csv.ts";
import type { KursorQiymati, VazifaOmbori }
from "../vazifalar/ombor.ts";
export type EksportHolati =
"kutilmoqda" | "bajarilmoqda" | "tayyor" | "xato";
type Ish = {
id: string; holat: EksportHolati; yaratildi: string;
tayyor: number; jami: number; csv?: string; xato?: string;
tugash: number;
};
const SAQLASH_MS = 60 * 60 * 1000; // natija 1 soat saqlanadi
const BOLAK = 100;
export function eksportRouteri(
ombor: VazifaOmbori,
{
soat = Date.now,
bolakdanKeyin = (): Promise<void> => navbat(),
} = {},
): Router {
const ishlar = new Map<string, Ish>();
const r = Router();
async function bajar(ish: Ish): Promise<void> {
ish.holat = "bajarilmoqda";
ish.jami = await ombor.soni({});
let csv = csvQator(["id", "matn", "bajarildi", "teglar",
"yaratildi", "yangilandi"]);
let keyin: KursorQiymati | undefined;
for (;;) {
const n = await ombor.royxat({
filtr: {}, saralash: [], limit: BOLAK,
sahifa: keyin === undefined
? { tur: "kursor" } : { tur: "kursor", keyin },
});
for (const v of n.vazifalar) {
csv += csvQator([v.id, v.matn, v.bajarildi,
v.teglar.join(" "), v.yaratildi, v.yangilandi]);
}
ish.tayyor += n.vazifalar.length;
if (!n.borKeyingi || n.keyingi === undefined) break;
keyin = n.keyingi;
// event loop'ni bo'shatamiz — boshqa so'rovlar kutib qolmasin
await bolakdanKeyin();
}
ish.csv = csv;
ish.holat = "tayyor";
ish.tugash = soat() + SAQLASH_MS;
}
function topilsin(id: string): Ish {
for (const [k, i] of ishlar) {
if (i.tugash <= soat()) ishlar.delete(k);
}
const ish = ishlar.get(id);
if (ish === undefined) {
throw new HttpXato(404,
`Eksport ${id} topilmadi (yoki muddati o'tgan)`,
{ kod: "eksport-topilmadi", sarlavha: "Eksport topilmadi" });
}
return ish;
}
function korinish(ish: Ish, asos: string): Record<string, unknown> {
const k: Record<string, unknown> = {
id: ish.id, holat: ish.holat, yaratildi: ish.yaratildi,
tayyor: ish.tayyor, jami: ish.jami,
};
if (ish.holat === "tayyor") k.fayl = `${asos}/${ish.id}/fayl`;
if (ish.xato !== undefined) k.xato = ish.xato;
return k;
}
r.post("/", (req, res) => {
const ish: Ish = {
id: randomUUID(), holat: "kutilmoqda",
yaratildi: new Date(soat()).toISOString(),
tayyor: 0, jami: 0, tugash: soat() + SAQLASH_MS,
};
ishlar.set(ish.id, ish);
// Javob kutmaydi: ish fonda; xatosi ish holatiga yoziladi
// (yutib yuborilmaydi)
bajar(ish).catch((x: unknown) => {
ish.holat = "xato";
ish.xato = "Eksport bajarilmadi";
console.error(`[eksport ${ish.id}]`, x);
});
res.status(202).location(`${req.baseUrl}/${ish.id}`)
.set("Retry-After", "1").json(korinish(ish, req.baseUrl));
});
r.get("/:id", (req, res) => {
const ish = topilsin(req.params.id);
if (ish.holat === "kutilmoqda" || ish.holat === "bajarilmoqda") {
res.set("Retry-After", "1");
}
res.set("Cache-Control", "no-store")
.json(korinish(ish, req.baseUrl));
});
r.get("/:id/fayl", (req, res) => {
const ish = topilsin(req.params.id);
if (ish.csv === undefined) {
throw new HttpXato(409, "Eksport hali tayyor emas", {
kod: "eksport-tayyor-emas", sarlavha: "Eksport tayyor emas",
httpSarlavhalar: { "Retry-After": "1" },
});
}
res.attachment(`vazifalar-${ish.yaratildi.slice(0, 10)}.csv`)
.type("text/csv; charset=utf-8").send(ish.csv);
});
return r;
}soat va bolakdanKeyin — tashqaridan beriladi: testda vaqtni boshqarish va bo'laklar orasidagi kutishni almashtirish mumkin. Ishlar 100 talik bo'laklar bilan, kursor orqali o'qiladi (pagination darsi) — ombor qaysi bo'lishidan qat'i nazar.
src/umumiy/csv.ts — to'liq:
// csv.ts — RFC 4180: vergul, qo'shtirnoq yoki yangi qator bo'lsa —
// qo'shtirnoq ichida, " → ""
export function csvMaydon(x: string | number | boolean): string {
const s = String(x);
return /[",\r\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
}
export function csvQator(
maydonlar: readonly (string | number | boolean)[],
): string {
return maydonlar.map(csvMaydon).join(",") + "\r\n";
}ilova.ts da ulanadi: v1.use("/eksportlar", eksportRouteri(ombor)).
Halol cheklov: ishlar xotirada — server qayta ishga tushsa, yo'qoladi. Ikki nusxali serverda ish bitta nusxada bo'ladi, holat so'rovi esa ikkinchisiga tushishi mumkin. Haqiqiy yechim — navbat (BullMQ, Redis) — navbat darslarida.
6.3 Natija (kanon curl, sarlavhalar qisqartirilgan)
curl -i -X POST $A/v1/eksportlar
curl $A/v1/eksportlar/<id>
curl -i $A/v1/eksportlar/<id>/faylHTTP/1.1 202 Accepted
Location: /v1/eksportlar/f4c82bf5-e649-45f1-83a4-cd1f8cf6cd66
Retry-After: 1
{"id":"f4c82bf5-…","holat":"bajarilmoqda","yaratildi":"2026-10-24T05:32:00.630Z","tayyor":0,"jami":0}
{"id":"4255e71a-…","holat":"tayyor","yaratildi":"2026-10-24T05:32:00.826Z","tayyor":4,"jami":4,"fayl":"/v1/eksportlar/4255e71a-…/fayl"}
HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename=vazifalar-2026-10-24.csv
id,matn,bajarildi,teglar,yaratildi,yangilandi
5,Import #uy,false,uy,2026-10-24T05:32:00.108Z,2026-10-24T05:32:00.108Z
6,"Non, sut va ""qatiq""",true,,2026-10-24T05:32:00.108Z,2026-10-24T05:32:00.108Z
7,A #ish,false,ish,2026-10-24T05:32:00.371Z,2026-10-24T05:32:00.371Z
8,B,true,,2026-10-24T05:32:00.371Z,2026-10-24T05:32:00.371ZDiqqat: ikkinchi va uchinchi javobning ID'si birinchisidan boshqa — kanon skripti tayyor holatni ko'rsatish uchun ikkinchi eksportni yaratib, uni kutgan. Ishning ID'si — randomUUID(): ketma-ket raqam bo'lsa, begona eksportni taxmin qilib yuklab olish oson bo'lardi (auth 28-qismda keladi).
Birinchi javobda holat bajarilmoqda, tayyor: 0. Nega namunadagidek kutilmoqda emas? Kanon bajar() birinchi qatorda holatni sinxron o'zgartiradi va faqat keyin ombor.soni() da await qiladi. korinish() 202 javobini shundan keyin yasaydi.
7. Ko'p uchraydigan xatolar
1. Uzoq ishni handler ichida bajarish. Proxy timeout (30–60 s), foydalanuvchi qayta bosadi — ish ikki marta boshlanadi.
2. Fondagi Promise'ni ushlamaslik. runExport(job) — .catch siz. Xato → unhandledRejection → jarayon o'ladi (kanon siyosati — jarayon xatolari darsi).
3. 207 ni res.ok bilan "muvaffaqiyat" deb o'qish. Har elementni tekshiring.
4. Bulk'da chegara yo'q. 1 000 000 elementli massiv — xotira va CPU tugaydi. express.json({ limit }) ham, elementlar soni ham cheklanadi.
5. Event loop'ni bloklovchi sikl. 50 000 qatorni await siz bitta siklda yig'ish — shu vaqtda server "o'lik".
6. Holat resursisiz 202. Location yo'q — mijoz ish qachon tugaganini bilolmaydi.
8. Mashqlar
Yechimlar kurs/mashqlar/25/27-bulk-lro/ da.
1-mashq (oson): to'ldiring
- Aralash natijali bulk javob statusi —
- "Qabul qilindi, ish hali tugamagan" —
- 202 da holat resursi manzili qaysi sarlavhada?
- Mijozga qancha kutishni aytuvchi sarlavha —
- Faylni yuklab olishga majburlovchi sarlavha —
Content-
Yechim
- 207 Multi-Status (RFC 4918).
- 202 Accepted.
- Location —
/v1/eksportlar/<id>. - Retry-After — soniyada.
- Disposition —
attachment; filename=….
2-mashq (o'rta): 207 dan qayta yuborish ro'yxati
retryList(sent, response) yozing: sent — yuborilgan elementlar massivi, response — 207 tanasi ({ results: [{ index, status, … }] }). Funksiya xato olgan (status >= 400) elementlarni o'z xabari bilan qaytarsin, ok sonini ham. Bu toza JS — «Ishga tushir» bilan ishlaydi.
Yechim
function retryList(sent, response) {
const failed = [];
let ok = 0;
for (const r of response.results) {
if (r.status < 400) {
ok++;
} else {
const why = r.error?.xabar ?? "?";
failed.push({ item: sent[r.index], why });
}
}
return { ok, failed };
}
const sent = [
{ matn: "A" }, { matn: "" }, { matn: "B" }, { matn: " " },
];
const response = {
results: [
{ index: 0, status: 201 },
{ index: 1, status: 422, error: { xabar: "matn bo'sh" } },
{ index: 2, status: 201 },
{ index: 3, status: 422, error: { xabar: "matn bo'sh" } },
],
};
const r = retryList(sent, response);
console.log(r.ok, r.failed.length);
console.log(r.failed.map((f) => JSON.stringify(f.item)).join(" "));Konsolda:
2 2
{"matn":""} {"matn":" "}Elementlar index bo'yicha topiladi — massiv tartibiga emas, javobdagi raqamga tayanamiz.
3-mashq (qiyin): chegarali polling
pollUntilDone(fetchStatus, { maxAttempts, baseMs, maxMs }) yozing. U fetchStatus() ni chaqirsin; state ready yoki failed bo'lsa — natijani qaytarsin. Aks holda kutsin: baseMs, keyin ikki baravar, lekin maxMs dan oshmasin. maxAttempts dan keyin Error tashlasin. Kutishlarni massivga yozib, soxta holat funksiyasi bilan sinang.
Yechim
async function pollUntilDone(fetchStatus, opts) {
const { maxAttempts, baseMs, maxMs, sleep } = opts;
let wait = baseMs;
for (let i = 1; i <= maxAttempts; i++) {
const status = await fetchStatus();
if (status.state === "ready" || status.state === "failed") {
return status;
}
if (i < maxAttempts) {
await sleep(wait);
wait = Math.min(wait * 2, maxMs);
}
}
throw new Error(`${maxAttempts} urinishda tugamadi`);
}
const waits = [];
const sleep = async (ms) => { waits.push(ms); };
let calls = 0;
const fakeStatus = async () => {
calls++;
if (calls < 5) return { state: "running" };
return { state: "ready", file: "/f" };
};
console.log(await pollUntilDone(fakeStatus, {
maxAttempts: 10, baseMs: 100, maxMs: 300, sleep,
}));
console.log(waits.join(", "));
calls = -100;
try {
await pollUntilDone(fakeStatus, {
maxAttempts: 3, baseMs: 100, maxMs: 300, sleep,
});
} catch (error) {
console.log(error.message);
}Konsolda:
{ state: 'ready', file: '/f' }
100, 200, 300, 300
3 urinishda tugamadisleep tashqaridan berildi — test haqiqiy vaqt kutmaydi va kutishlar ro'yxatini tekshira oladi (vaqtni boshqarish shu g'oyani rivojlantiradi).
4-mashq: API qadami
O'z vazifalar-api ingizda «API qadami» bo'limidagi uch narsani qiling: atomik PUT /v1/vazifalar (+ almashtirHammasi, to'plam versiya), POST /v1/vazifalar/toplam (207), src/eksport/eksport.ts (202 → polling → CSV). Testlar: PUT to'plam (takror id → yangi, versiya oshadi, 422 da ombor o'zgarmaydi), toplam 207/201/422, eksport 202 → polling → CSV qatorlari soni = jami + 1. Commit qilmang — keyingi darsda API.md bilan birga.
9. Real ishda
- Google Cloud API'lari (AIP 151-band) uzoq ishlarni
Operationresursi bilan qaytaradi:done,metadata,response— aynan shu naqsh. - GitHub repository arxivi, Telegram kanal eksporti, bank ko'chirmalari — "tayyorlanmoqda, keyin havola beramiz".
- Elasticsearch
_bulk— qisman muvaffaqiyatli bulk namunasi: har element o'z statusini oladi. Lekin javobning o'zi 207 emas — 200 va"errors": truebayrog'i. Naqsh bir xil, kodi boshqa: mijoz baribir har elementni tekshiradi. - Fon ishlari ishonchli bo'lishi uchun navbat kerak: BullMQ (BullMQ asoslari) qayta urinish, ustuvorlik va jarayon qulashidan keyin davom etishni beradi.
- Intervyu: "5 daqiqalik hisobotni API orqali qanday berasiz?" — 202 + holat resursi + polling/webhook + natija havolasi + muddat.
Xulosa
- Bulk: atomik (422, hech narsa yozilmaydi) yoki qisman (207, har element o'z statusi bilan) — vaziyatga qarab.
- 207 ni o'qiganda har element tekshiriladi;
res.okyetmaydi. - Uzoq ish — 202 +
Location(holat resursi) +Retry-After; ish fonda, bo'laklar bilan, event loop bo'shatiladi. - Fondagi xato holatga yoziladi va logga chiqadi — yutilmaydi.
- Polling — oddiy va brauzer uchun; webhook — serverdan serverga; natija fayli
Content-Dispositionbilan va muddatli.
Keyingi dars: API dizayn qo'llanmalari va konvensiyalar — nomlash, sana, ID, null, enum, pul kabi mayda qarorlarni bir joyga yig'amiz va API.md yozib, holat 27 ni commit qilamiz.
Manbalar
- RFC 9110 §15.3.3 "202 Accepted" (2022-06) — rfc-editor.org/rfc/rfc9110
- RFC 4918 §11.1 "207 Multi-Status" (2007-06) — rfc-editor.org/rfc/rfc4918
- RFC 4180 "Common Format and MIME Type for CSV Files" (2005-10) — rfc-editor.org/rfc/rfc4180
- RFC 6266
Content-DispositionHTTP'da (2011-06) — rfc-editor.org/rfc/rfc6266 - Google AIP 151 "Long-running operations" — google.aip.dev/151
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!