جلسههای طولانی AI رو با Durable Objects زنده نگه دار
خلاصهٔ کاملتر
نویسنده قبلاً دربارهٔ AI SDK و AI Gateway ورسل نوشته بود و حالا سراغ مشکل UX همون نمونه رفته. تو اپ ردیاب تمرین خودش، کاربر پرامپت میداد و یه مودال با اسپینر نشون داده میشد تا جواب مدل برسه، که معمولاً ۳۰ ثانیه یا بیشتر طول میکشید. مشکل واضحه: رفرش صفحه وسط درخواست، یا حتی بعد از رسیدن نتیجه، همهچی رو میپروند.
راهحل Durable Objects کلادفلره. به گفتهٔ نویسنده، یه DO مثل Worker معمولیه با این تفاوت که ephemeral نیست: عمر طولانی داره، حافظهٔ پایدار SQLite داره، پشتیبانی WebSocket توش تعبیه شده و وقتی کاری نیست مجانی hibernate میکنه. هر DO رو با یه شناسهٔ دلخواه میسازی، مثلاً یکی بهازای هر کاربر، و هر نمونه دیتابیس SQLite و مجموعه کانکشنهای WebSocket خودش رو داره.
همین نمونهسازی بهازای هر کاربر، ایزولاسیون داده رو مجانی میده: چون DO با userId ساخته میشه، هر کاربر فقط دیتابیس خودشو میبینه و کوئریها لازم نیست فیلتر کاربر داشته باشن.
export const getWorkoutTemplateAIGenerationDurableObject = async (context: AuthContext) => {
const userId = await requireUserId(context);
const { WorkoutTemplateAIGenerationDO } = env;
const doId = WorkoutTemplateAIGenerationDO.idFromName(userId);
return WorkoutTemplateAIGenerationDO.get(doId);
};جریان کار اینه: کاربر دکمهٔ Generate رو میزنه، یه server function جلسه رو تو DO میسازه، پرامپت رو شروع میکنه و بلافاصله شناسهٔ جلسه رو برمیگردونه بدون اینکه منتظر مدل بمونه. بعد کاربر به صفحهٔ همون جلسه ریدایرکت میشه، تاریخچهٔ پرامپتها از SQLite لود میشه و یه WebSocket وصل میشه. وقتی جواب مدل رسید، DO اون رو به همهٔ سوکتهای بازش پوش میکنه.
sendMessage(payload: Object) {
for (const socket of this.ctx.getWebSockets()) {
try {
socket.send(JSON.stringify(payload));
} catch {
socket.close(1011, "Unable to send message");
}
}
}دو نکتهٔ عملی دیگه هم هست. برای گرفتن کانکشن خام WebSocket به DO، نویسنده از یه API route استفاده میکنه، همون چیزی که تو TanStack بهش میگن server route، و این روت درخواست رو صاف به DO پراکسی میکنه. داخل constructor هم با ctx.blockConcurrencyWhile مایگریشن SQL قفلشده اجرا میشه تا وقتی اسکیما ناقصه درخواست دیگهای جلو نره؛ به همین خاطر DDL با CREATE TABLE IF NOT EXISTS نوشته شده. دسترسی به داده هم با Drizzle انجام میشه و API سینکرون SQLite یعنی حتی await لازم نیست.
نکات کلیدی:
- Durable Object برعکس Worker معمولی عمر طولانی داره، SQLite اختصاصی و WebSocket داخلی میده و موقع بیکاری مجانی hibernate میکنه
- هر DO با یه شناسهٔ دلخواه ساخته میشه؛ تو این پروژه یکی بهازای هر userId
- کل کلاس DO حدود ۲۵۰ خطه و کدش تو ریپوی fitness-tracker روی برنچ feature/ai-workout-template-generation هست
- ctx.blockConcurrencyWhile تو constructor مایگریشن DDL رو قفلشده اجرا میکنه
- API سینکرون SQLite یعنی کوئریهای داخل DO نیازی به async و await ندارن
- پرامپت پیشفرض با مدل anthropic/claude-sonnet-4.6 از طریق generateText صدا زده میشه
- یه اسکیمای Zod خروجی مدل رو به همون ساختاری که بقیهٔ اپ استفاده میکنه محدود میکنه، پس مستقیم تو کامپوننتهای فرم موجود جا میشه




