ویرگول
ورودثبت نام
garmabi.mohammad73
garmabi.mohammad73Senior Software Engineer با تمرکز بر Frontend، React و TypeScript. علاقه‌مند به Software Architecture، System Design، Open Source و ساخت ابزارهای ساده و مقیاس‌پذیر.
garmabi.mohammad73
garmabi.mohammad73
خواندن ۴ دقیقه·۳ روز پیش

انتشار tanstack-fetch 2.1.0؛ لغو واقعی در retry، رفرش توکن امن‌تر و پارامترهای آرایه‌ای

نسخه اصلی این مطلب (انگلیسی): mohamadgarmabi.github.io/tanstack-fetch/blog/tanstack-fetch-2-1-0

نسخه 2.1.0 کتابخانه tanstack-fetch یک نسخه با محوریت رفع باگ است و هیچ تغییر ناسازگاری (breaking change) ندارد. با ارتقا این‌ها را می‌گیرید:

  • لغو یعنی لغو: اگر درخواست در حین تأخیرِ بین تلاش‌ها (retry delay) لغو شود، بلافاصله reject می‌شود.

  • دونقطه در مسیر: مسیری مثل /models/gemini:generateContent دیگر پارامتری به اسم generateContent نمی‌خواهد.

  • فقط یک رفرش برای هر چرخش توکن: یک 401 دیررس دیگر رفرش دوم را شروع نمی‌کند.

  • مقادیر آرایه‌ای در query، گزینه‌های parseAs: 'arrayBuffer' | 'stream' و entry جدید tanstack-fetch/full.

ارتقا

npm install [email protected]

باگ‌های رفع‌شده

لغو درخواست در حین تأخیر retry

وقتی سرور Retry-After: 5 برمی‌گرداند (یا هر backoff دیگری)، درخواست HTTP حتی بعد از abort شدن signal کل زمان تأخیر را صبر می‌کرد. TanStack Query موقع unmount و تغییر queryKey درخواست را abort می‌کند، پس query چند ثانیه در حالت pending می‌ماند. حالا تأخیر به signal گوش می‌دهد و بلافاصله با AbortError رد می‌شود و تلاش بعدی هم ارسال نمی‌شود.

useQuery({ queryKey: ['report', id], // unmount during a 5s Retry-After → rejects now, no second fetch queryFn: ({ signal }) => api.get('/reports/:id', { params: { id }, signal }), })

دونقطه داخل یک بخش از مسیر

حالا :name فقط وقتی در ابتدای یک بخش از مسیر بیاید پارامتر مسیر حساب می‌شود؛ هم در زمان اجرا و هم در تایپ‌ها:

await api.post('/v1/models/gemini:generateContent', { body }) // literal path, no params await api.get('/users/:id', { params: { id: 1 } }) // → /users/1

قبلاً فراخوانی اول با خطای missing path param "generateContent" شکست می‌خورد و TypeScript هم همین پارامتر را می‌خواست.

رفرش توکن: بدون رفرش دوم برای 401 دیررس

interceptor از قبل یک رفرش در جریان را بین چند 401 موازی به اشتراک می‌گذاشت. اما درخواستی که با توکن قدیمی ارسال شده بود و 401اش بعد از تمام شدن رفرش می‌رسید، یک رفرش جدید شروع می‌کرد. با refresh tokenهای چرخشی (rotating)، این فراخوانی دوم می‌تواند نشستی را که اولی تازه ساخته باطل کند.

حالا interceptor می‌داند هر درخواست بعد از کدام رفرش ارسال شده است. یک 401 دیررس فقط درخواست را با توکن جدید دوباره می‌فرستد:

A ──(old token)──────────────────────► 401 → retry with new token ✓ B ──(old token)──► 401 → refresh() ✓ → retry ✓ refresh() called once

حالت برعکس هم درست شده است. درخواستی که به دلیل دیگری retry شده بود (مثلاً یک 502 از طریق retry-idempotent) و بعد 401 می‌گرفت، رفرش را رد می‌کرد، چون شرط بررسی attempt > 0 بود. حالا هر درخواست حداکثر یک بار رفرش می‌کند و این به ازای هر فراخوانی ردیابی می‌شود، پس چرخه 401 → refresh → 401 همچنان نمی‌تواند حلقه بسازد.

این قابلیت با یک context.meta.state جدید کار می‌کند؛ یک object ساده که بین همه تلاش‌های یک درخواست مشترک است. interceptorهای خودتان هم می‌توانند از آن برای flagهای مخصوص هر فراخوانی استفاده کنند.

بدنه‌های stream دیگر retry نمی‌شوند

بدنه از نوع ReadableStream در تلاش اول مصرف می‌شود، بنابراین retry قبلاً با یک TypeError گیج‌کننده درباره «locked stream» شکست می‌خورد. حالا درخواست‌هایی که بدنه stream دارند، به جای retry، خطای واقعی (مثلاً FetchError با وضعیت 503) را برمی‌گردانند.

قابلیت‌های جدید

مقادیر آرایه‌ای در query

await api.get('/users', { query: { ids: [1, 2], role: 'admin' } }) // → /users?ids=1&ids=2&role=admin

QueryParams حالا آرایه‌ای از string | number | boolean را می‌پذیرد و هر آیتم به صورت یک key=value جدا اضافه می‌شود. مقادیر null و undefined همچنان نادیده گرفته می‌شوند.

گزینه parseAs: 'arrayBuffer' | 'stream'

const bytes = await api.get<ArrayBuffer>('/files/:id', { params: { id }, parseAs: 'arrayBuffer' }) const body = await api.get<ReadableStream<Uint8Array>>('/export', { parseAs: 'stream' })

حالت 'stream' خودِ response.body را بدون خواندن برمی‌گرداند.

entry جدید tanstack-fetch/full

کلاینت کامل HTTP (میان‌برهای option، تابع upload() و رویدادهای progress) قبلاً فقط زیر tanstack-fetch/sse بود که اگر از SSE استفاده نمی‌کنید عجیب به نظر می‌رسد. tanstack-fetch/full همان build با یک اسم واضح‌تر است و /sse هم مثل قبل کار می‌کند.

import { createFetch } from 'tanstack-fetch/full'

تایپ‌های درست برای CommonJS

حالا require('tanstack-fetch') به جای .d.ts نسخه ESM، به declarationهای .d.cts resolve می‌شود؛ بنابراین پروژه‌های TypeScript که با module: node16 یا nodenext و CJS کار می‌کنند تایپ‌های درست را می‌گیرند. CI هم حالا همه entryها را با publint و @arethetypeswrong/cli بررسی می‌کند و تست‌ها را روی Node 20، 22 و 24 اجرا می‌کند.

حجم باندل (gzip)

  • tanstack-fetch: از 3.57KB به 3.68KB

  • tanstack-fetch/sse: از 6.33KB به 6.43KB

  • tanstack-fetch/live: از 8.75KB به 8.84KB

  • tanstack-fetch/plugins: از 1.13KB به 1.22KB

یعنی حدود ۱۰۰ بایت برای همه این اصلاحات.

نکته

timeoutها و abortها با TimeoutError / AbortError خودِ پلتفرم reject می‌شوند، نه با FetchError. این همیشه همین‌طور بوده و حالا در README هم ذکر شده است. برای بررسی‌شان از isAbortError(error) استفاده کنید. (مستندات Errors)

بیشتر بخوانید: آموزش refresh token · گزینه‌های درخواست · Changelog · مخزن GitHub

typescriptreactبرنامه نویسی
۱
۰
garmabi.mohammad73
garmabi.mohammad73
Senior Software Engineer با تمرکز بر Frontend، React و TypeScript. علاقه‌مند به Software Architecture، System Design، Open Source و ساخت ابزارهای ساده و مقیاس‌پذیر.
شاید از این پست‌ها خوشتان بیاید