كل درس عن الحوسبة بلا خوادم يبدأ بالفصل الأول الممل نفسه. ثبّت أداة سطر الأوامر. أنشئ دور IAM. اكتب ملف stack يُنشئ جدول DynamoDB. اكتب ملفًا ثانيًا يمنح دالة Lambda صلاحية القراءة من ذلك الجدول. اكتب ملفًا ثالثًا يربط مسار API Gateway بدالة Lambda. ولّد حزمة SDK للعميل. اضبط عنوان الـ endpoint في ملف .env الخاص بالواجهة. ثم، بعد أربعين دقيقة، اكتب الأسطر الستة من منطق العمل التي أردت كتابتها فعلًا.
AWS Blocks، الذي أطلقته أمازون كمعاينة عامة في 16 يونيو 2026، يحذف هذا الفصل الأول تمامًا. إنه إطار عمل TypeScript مفتوح المصدر يكون فيه سطر واحد — new DistributedTable(scope, 'todos', { schema, key }) — هو في آن واحد تعريف البنية التحتية، وواجهة التشغيل، وتطبيقًا محليًا يعمل في الذاكرة دون أي حساب AWS. السطر نفسه يصبح جدول DynamoDB مع فهارسه الثانوية عند النشر، ونداء AWS SDK داخل Lambda عند التشغيل.
الحيلة هي الصادرات الشرطية في Node.js. حين تستورد Block، يسلّمك محلّل الوحدات ملفًا مختلفًا حسب سياق التنفيذ: محاكاة في الذاكرة أثناء npm run dev، وconstruct من CDK أثناء التوليف، وغلاف SDK داخل Lambda. أنت لا تضبط هذه الآلية أبدًا. ولا تراها أبدًا. أنت تكتب شيفرة التطبيق فحسب، والبنية التحتية تُشتق منها.
يبني هذا الدرس تطبيقًا حقيقيًا: متتبّع مهام بمصادقة، مع استعلامات مهيكلة، وتحديثات WebSocket حية، ومرفقات ملفات، ومهمة تنظيف ليلية، ومساعد ذكاء اصطناعي مدعوم بـ Amazon Bedrock. كل شيء يعمل على جهازك أولًا. النشر هو الفصل الأخير، وهو ثلاثة أوامر لا أكثر.
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 22 أو أحدث وnpm 10 أو أحدث. يعتمد AWS Blocks على
Array.fromAsyncوعلى الحل الحديث للصادرات الشرطية، لذا تفشل الإصدارات الأقدم بطرق مربكة. تحقق بـnode --version. - معرفة عملية بـ TypeScript. ينبغي أن تكون مرتاحًا مع الأنواع العامة والمكرّرات غير المتزامنة. أما AWS CDK وCloudFormation ونمذجة DynamoDB فلست بحاجة إليها إطلاقًا.
- محرر يدعم TypeScript — VS Code أو Kiro أو أي محرر بخادم لغة TypeScript. جزء كبير من قيمة هذا الإطار هو انسياب الإكمال التلقائي من الخلفية إلى الواجهة.
- اختياري، لفصل النشر فقط: حساب AWS، وAWS CLI الإصدار 2 مضبوطة، وCDK مُهيّأ بـ bootstrap. الخطوات من 1 إلى 7 لا تحتاج أيًّا من ذلك.
الخطوات من 1 إلى 7 لا تتطلب أي حساب AWS، ولا بيانات اعتماد، ولا اتصالًا بالإنترنت. يمكنك إكمال التطبيق بالكامل دون اتصال. لا تدخل AWS في الصورة إلا في الخطوة 8.
ما ستبنيه
متتبّع مهام متعدد المستخدمين، حيث يملك كل مستخدم مسجَّل الدخول مهامه الخاصة، ويرى التحديثات تُدفع إليه حيًّا عبر تبويبات المتصفح، ويستطيع إرفاق ملف بمهمة، وتُؤرشف مهامه القديمة تلقائيًا كل ليلة، ويمكنه أن يطلب من مساعد ذكاء اصطناعي تلخيص حجم عمله.
بترجمة ذلك إلى Blocks:
| الميزة | الـ Block | محليًا | على AWS |
|---|---|---|---|
| تسجيل الدخول باسم وكلمة مرور | AuthBasic | JWT محلي | سجلات DynamoDB |
| تخزين المهام والاستعلام عنها | DistributedTable | في الذاكرة | DynamoDB + فهرس ثانوي |
| واجهة برمجية آمنة الأنواع | ApiNamespace | خادم HTTP محلي | API Gateway + Lambda |
| التحديثات الحية | Realtime | EventEmitter | API Gateway WebSocket |
| مرفقات الملفات | FileBucket | مجلد .bb-data/ | S3 مع روابط موقّعة |
| الأرشفة الليلية | CronJob | مؤقتات Node | EventBridge Scheduler |
| مساعد الذكاء الاصطناعي | Agent | مزوّد محاكى | Amazon Bedrock |
سبع قدرات خلفية، ملف واحد، وصفر YAML.
الخطوة 1: تجهيز المشروع
يوفّر AWS Blocks حزمة create. شغّلها:
npm create @aws-blocks/blocks-app@latest task-tracker
cd task-tracker
npm installستحصل على شجرة ملفات صغيرة عن قصد:
task-tracker/
├── aws-blocks/
│ └── index.ts # الخلفية: الـ Blocks وتعريفات الواجهة البرمجية
├── src/
│ └── index.ts # الواجهة الأمامية
├── index.html
└── package.json
مجلدان اثنان. الملف aws-blocks/index.ts هو ما تسميه الوثائق طبقة IFC — أي البنية التحتية المشتقة من الشيفرة. وهو في آن واحد تعريف بنيتك التحتية وزمن تشغيل خلفيتك. أما src/ فهو واجهة أمامية عادية تستورد الخلفية مباشرة.
تتوفر قوالب أخرى عبر --template: منها nextjs وreact وauth-cognito وdemo وbare وbackend وamplify. وتشغيل الأمر داخل مشروع قائم — بتمرير . كمجلد — يضيف إليه خلفية aws-blocks/، وتكتشف الأداة تلقائيًا وجود مشروع Amplify Gen 2 إن وُجد.
شغّل خادم التطوير:
npm run devافتح http://localhost:3000. سيكون تطبيق مهام كامل مع التسجيل وتسجيل الدخول وعمليات CRUD يعمل بالفعل — كله داخل العملية نفسها، دون أي اتصال سحابي. وإعادة التحميل الفوري مفعّلة.
الخطوة 2: فهم الـ Scope والـ Blocks
افتح aws-blocks/index.ts. كل شيء يبدأ بـ Scope:
import { Scope, KVStore } from '@aws-blocks/blocks';
const scope = new Scope('task-tracker');
const cache = new KVStore(scope, 'cache', {});
const sessions = new KVStore(scope, 'sessions', {});
// المعرّفات الكاملة: task-tracker/cache و task-tracker/sessionsالـ Scope حاوية أسماء. يجب إنشاء كل Block داخل واحدة، ويُشتق المعرّف الكامل للـ Block من اسم الـ scope مضافًا إليه المعرّف الذي تمرره كوسيط ثانٍ. وهذا المعرّف هو ما يحدد اسم مورد AWS الفعلي.
معرّفات الـ Blocks دائمة. إعادة تسمية الوسيط الثاني للمُنشئ بعد النشر تدفع CloudFormation إلى حذف المورد الأساسي وإعادة إنشائه. وبالنسبة إلى الـ Blocks ذات الحالة — KVStore وDistributedTable وDatabase وFileBucket — فهذا يعني فقدانًا لا رجعة فيه للبيانات. تعامل مع معرّفات الـ Blocks على أنها ثابتة بمجرد وصولها إلى أي بيئة تهتم بها.
يعرض كائن KVStore نفسه واجهة متطابقة في السياقات الثلاثة:
await cache.put('user:123', { name: 'Alice' });
const user = await cache.get('user:123');محليًا، يكتب ذلك في مجلد .bb-data/ بجذر مشروعك. وداخل Lambda، هو عملية PutItem على DynamoDB. شيفرتك لا تعرف ذلك ولا يعنيها.
الخطوة 3: نمذجة المهام باستخدام DistributedTable
يتولى KVStore عمليات البحث حين تعرف المفتاح دائمًا. لكن متتبّع مهامنا يحتاج إلى الاستعلام بحسب المستخدم وبحسب الحالة، لذا نريد DistributedTable: تخزينًا مهيكلًا مُتحقَّقًا منه بمخطط، مع فهارس ثانوية.
استبدل محتوى aws-blocks/index.ts:
import { ApiNamespace, Scope, DistributedTable, AuthBasic } from '@aws-blocks/blocks';
import { z } from 'zod';
const scope = new Scope('task-tracker');
const auth = new AuthBasic(scope, 'auth', {
sessionDuration: 86400,
passwordPolicy: { minLength: 8, requireDigits: true },
});
export const authApi = auth.createApi();
const taskSchema = z.object({
userId: z.string(),
taskId: z.string(),
title: z.string().min(1).max(200),
status: z.enum(['open', 'done', 'archived']),
createdAt: z.string(),
attachmentPath: z.string().optional(),
});
const tasks = new DistributedTable(scope, 'tasks', {
schema: taskSchema,
key: { partitionKey: 'userId', sortKey: 'taskId' },
indexes: {
byStatus: { partitionKey: 'status', sortKey: 'createdAt' },
},
});حدثت ثلاثة أمور. صار مخطط Zod أنواعًا في زمن الترجمة وتحققًا في زمن التشغيل معًا. وصارت إعدادات key هي المفتاح الأساسي في DynamoDB. وصار قسم indexes فهرسًا ثانويًا عامًا — ونمذجة فهارس DynamoDB الثانوية، وهي عادةً أصعب جزء في أي مشروع بلا خوادم، صارت أربع كلمات.
لا تمرر أبدًا وسيط نوع صريحًا واحدًا إلى DistributedTable. كتابة new DistributedTable<Task>(...) تثبّت نوع العنصر وحده وتترك أنواع المفتاح والفهرس تعود إلى قيمها الافتراضية الفضفاضة، فينكسر استنتاج المفتاح: عندئذ سيطالب get() بـكل حقول نوعك بدل حقول المفتاح فقط. اترك الأنواع العامة تُستنتج كلها، أو مرّرها الثلاثة معًا. وإضافة as const وحدها لا تصلح المشكلة.
دلالات الاستعلام
يعيد query() كائن AsyncIterable لا مصفوفة. وهذا مقصود: فهو ينعكس مباشرة على نموذج الاستعلامات المُقسَّمة إلى صفحات في DynamoDB، فتحصل على الترقيم التلقائي دون كتابة حلقة مؤشرات.
// استعلام على المفتاح الأساسي — لاحظ معاملات `where` آمنة الأنواع
for await (const task of tasks.query({
where: { userId: { equals: 'alice' }, taskId: { beginsWith: '2026-' } },
})) {
console.log(task.title);
}
// استعلام على الفهرس الثانوي
for await (const task of tasks.query({
index: 'byStatus',
where: { status: { equals: 'open' } },
limit: 25,
order: 'desc',
})) {
console.log(task.title);
}لجمع النتائج دفعة واحدة، استخدم await Array.fromAsync(tasks.query({ ... })). وفضّل query() على scan() — فالمسح يقرأ كل عنصر في الجدول.
طرق البيانات مخصصة لزمن التشغيل فقط. استدعاء tasks.put() أو tasks.query() أو rt.publish() وأخواتها في المستوى الأعلى من aws-blocks/index.ts يطلق الخطأ tasks.put is not a function. فشيفرة المستوى الأعلى تُنفَّذ أثناء توليف CDK، حيث يتحول الـ Block إلى construct بنية تحتية بلا أي طرق بيانات. أما إنشاء Block على مستوى الوحدة فسليم تمامًا — الاستدعاءات وحدها هي التي يجب أن تعيش داخل معالِج. ولزرع بيانات أولية، افعل ذلك من داخل طريقة في الواجهة البرمجية أو من سكربت تشغيل منفصل.
الخطوة 4: كشف واجهة برمجية آمنة الأنواع عبر ApiNamespace
ApiNamespace هو الجسر من المتصفح إلى الخلفية. إنه RPC لا REST: تُعرّف طرقًا، وتستدعيها الواجهة، ويتحقق TypeScript من الطرفين. لا توجد خطوة توليد شيفرة ولا ملف OpenAPI.
أضف إلى aws-blocks/index.ts:
export const api = new ApiNamespace(scope, 'api', (context) => ({
async createTask(title: string) {
const user = await auth.requireAuth(context);
const task = {
userId: user.username,
taskId: crypto.randomUUID(),
title,
status: 'open' as const,
createdAt: new Date().toISOString(),
};
await tasks.put(task);
return task;
},
async listTasks() {
const user = await auth.requireAuth(context);
return await Array.fromAsync(
tasks.query({ where: { userId: { equals: user.username } } })
);
},
async completeTask(taskId: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task) throw new Error('Task not found');
const updated = { ...task, status: 'done' as const };
await tasks.put(updated);
return updated;
},
async deleteTask(taskId: string) {
const user = await auth.requireAuth(context);
await tasks.delete({ userId: user.username, taskId });
},
}));
export { auth };المعامل context هو BlocksContext — الكائن المرافق لكل طلب والحامل للترويسات والكوكيز. أنت لا تنشئه أبدًا؛ يوفّره الإطار لكل طلب وارد. وتستخدمه Blocks المصادقة لقراءة كوكي الجلسة، ولهذا فإن auth.requireAuth(context) هو السطر الوحيد الذي يؤمّن طريقة كاملة. يطلق هذا السطر استثناء SessionExpiredException برمز 401 حين لا توجد جلسة صالحة، فلا يصل نداء غير موثّق إلى طبقة التخزين لديك أبدًا.
لاحظ نمط تعدد المستأجرين: user.username هو مفتاح التقسيم في كل قراءة وكتابة. لا يستطيع المستخدم فعليًا الاستعلام عن صفوف مستخدم آخر، لأن مفتاح التقسيم مُشتق من الجلسة الموثَّقة لا من وسيط يرسله العميل.
استدعاؤها من الواجهة الأمامية
في src/index.ts:
import { api, authApi } from 'aws-blocks';
const task = await api.createTask('نشر الدرس');
console.log(task.taskId);
const all = await api.listTasks();هذا هو تكامل العميل بأكمله. لا عنوان أساسي، ولا غلاف fetch، ولا SDK مولَّد، ولا عناء مع ترويسة Authorization. غيّر createTask لتأخذ وسيطًا ثانيًا في الخلفية، وستظهر لك واجهتك خطأ ترجمة قبل أن تفتح المتصفح أصلًا.
الخطوة 5: دفع التحديثات الحية عبر Realtime
يجب أن يبقى تبويبان في المتصفح متزامنين. يمنحك Realtime نشرًا واشتراكًا عبر WebSocket بأنواع محددة وحمولات مُتحقَّق منها بـ Zod.
import { Realtime } from '@aws-blocks/blocks';
const rt = new Realtime(scope, 'live', {
namespaces: {
tasks: Realtime.namespace(
z.object({
event: z.enum(['created', 'completed', 'deleted']),
taskId: z.string(),
title: z.string(),
})
),
},
});يحدث النشر من جهة الخادم، داخل طريقة في الواجهة البرمجية — أي في المكان نفسه الذي يعيش فيه منطق التخويل لديك أصلًا:
async createTask(title: string) {
const user = await auth.requireAuth(context);
const task = { /* ... كما سبق ... */ };
await tasks.put(task);
await rt.publish('tasks', user.username, {
event: 'created',
taskId: task.taskId,
title,
});
return task;
},الوسيط الثاني هو القناة. واستخدام user.username اسمًا للقناة يمنح كل مستخدم تدفقًا خاصًا داخل مساحة الأسماء المشتركة tasks.
مقابض القنوات مخصصة للاشتراك فقط عن قصد — فليست لها طريقة publish() — بحيث لا يستطيع العميل البث مباشرة أبدًا. والنمط الموصى به هو حجب الاشتراك خلف طريقة في الواجهة البرمجية لا تعيد المقبض إلا بعد التحقق من الصلاحيات:
async subscribeToMyTasks() {
const user = await auth.requireAuth(context);
return rt.getChannel('tasks', user.username);
},في جهة العميل، اشترك وانتظر دائمًا established قبل الاعتماد على الاتصال. يتحقق هذا الوعد بعد اكتمال مصافحة WebSocket والتخويل من جهة الخادم معًا، ويُرفض عند فشل المصادقة:
const channel = await api.subscribeToMyTasks();
const sub = channel.subscribe((msg) => {
console.log(`${msg.event}: ${msg.title}`);
refreshTaskList();
});
await sub.established;
// لاحقًا
sub.unsubscribe();محليًا، يعمل هذا على EventEmitter داخل العملية مع خادم WebSocket محلي. وعلى AWS يصبح واجهة WebSocket عبر API Gateway مع إدارة الاتصالات في DynamoDB. صُمم Realtime لقنوات تضم من عشرات إلى بضعة آلاف من المشتركين المتزامنين — إذ يتزايد زمن النشر خطيًا تقريبًا، نحو 100 ميلي ثانية لألف مشترك. وبعد عشرة آلاف مشترك في القناة الواحدة، ستحتاج إلى توزيع مُجزّأ صريح بدلًا من ذلك.
الخطوة 6: المرفقات والأعمال المجدولة
FileBucket
يجب ألا تمر المرفقات عبر دالة Lambda لديك. يصدر FileBucket روابط موقّعة مسبقًا ليرفع المتصفح مباشرة إلى S3:
import { FileBucket } from '@aws-blocks/blocks';
const attachments = new FileBucket(scope, 'attachments', {
corsRules: [
{
allowedOrigins: ['http://localhost:3000'],
allowedMethods: ['GET', 'PUT'],
allowedHeaders: ['*'],
},
],
lifecycleRules: [{ prefix: 'tmp/', expirationDays: 7 }],
removalPolicy: 'destroy',
});ثم داخل مساحة أسماء الواجهة البرمجية:
async getUploadUrl(taskId: string, fileName: string) {
const user = await auth.requireAuth(context);
const path = `${user.username}/${taskId}/${fileName}`;
const url = await attachments.putUrl(path);
return { url, path };
},
async attachFile(taskId: string, path: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task) throw new Error('Task not found');
await tasks.put({ ...task, attachmentPath: path });
},
async getDownloadUrl(taskId: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task?.attachmentPath) return null;
return attachments.getUrl(task.attachmentPath);
},لاحظ أن getUrl() وget() يعيدان null للملف المفقود بدل إطلاق استثناء — فتحقق من القيمة الفارغة صراحة. ومحليًا، تستقر الملفات في .bb-data/ على نظام ملفاتك، بمحاكاة سلوك واجهة S3 بدقة تكفي لأن تعمل تدفقات الروابط الموقّعة بالطريقة نفسها.
اضبط removalPolicy: 'destroy' على حزم الاختبار والبيئات المؤقتة فقط. فالسلوك الافتراضي في CDK لحاويات S3 هو الاحتفاظ (RETAIN)، وهو ما تريده في الإنتاج: إذ يمنع أمر npm run destroy العرضي من حذف بيانات مستخدميك.
CronJob
أرشِف كل ما بقي مفتوحًا أكثر من 30 يومًا:
import { CronJob } from '@aws-blocks/blocks';
const archiveStale = new CronJob(scope, 'archive-stale', {
schedule: 'cron(0 3 * * ? *)',
timezone: 'Africa/Tunis',
description: 'أرشفة المهام المفتوحة منذ أكثر من 30 يومًا',
handler: async (event) => {
const cutoff = new Date(Date.now() - 30 * 86400_000).toISOString();
for await (const task of tasks.query({
index: 'byStatus',
where: { status: { equals: 'open' }, createdAt: { lessThan: cutoff } },
})) {
await tasks.put({ ...task, status: 'archived' });
}
},
});تتكون تعبيرات cron في EventBridge من ستة حقول — cron(الدقيقة الساعة يوم-الشهر الشهر يوم-الأسبوع السنة) — ويجب أن يكون أحد حقلي يوم-الشهر أو يوم-الأسبوع هو ?. أما للفواصل الزمنية البسيطة، فتعبيرات المعدل أوضح: rate(5 minutes) وrate(1 hour) وrate(7 days). ويجب أن تكون معالجات cron مكافئة عند التكرار، لأن EventBridge يضمن التسليم مرة واحدة على الأقل، فلا يجوز أن يُفسد استدعاء مزدوج الحالة. والحلقة أعلاه آمنة، لأن ضبط status على 'archived' مرتين يعادل ضبطها مرة واحدة.
AsyncJob
للأعمال التي تُطلق وتُنسى، والتي يبدأها إجراء مستخدم لا ساعة مجدولة، استخدم AsyncJob:
import { AsyncJob } from '@aws-blocks/blocks';
const notify = new AsyncJob(scope, 'notify', {
schema: z.object({ to: z.string().email(), taskTitle: z.string() }),
maxRetries: 3,
handler: async (payload, ctx) => {
console.log(`المهمة ${ctx.jobId}، المحاولة ${ctx.receiveCount}`);
await sendEmail(payload.to, `اكتملت المهمة: ${payload.taskTitle}`);
},
});
// من طريقة في الواجهة البرمجية — تعود فورًا
const { jobId } = await notify.submit({ to: user.username, taskTitle: title });تعود submit() بمجرد وضع الرسالة في الطابور، فلا تتعطل استجابة الواجهة البرمجية. وعلى AWS يوفّر ذلك طابور SQS وطابور رسائل ميتة؛ أما محليًا فيعمل داخل العملية عبر setTimeout، مع تطبيق إعادة المحاولات وسلوك طابور الرسائل الميتة وحد الحمولة البالغ 256 كيلوبايت بالطريقة نفسها. استخدم submitBatch() لما يصل إلى عشر حمولات دفعة واحدة.
الخطوة 7: إضافة مساعد ذكاء اصطناعي
يعتمد الـ Block المسمى Agent على حزمة Strands Agents SDK، ويمنحك البث التدريجي واستدعاء الأدوات وموافقة الإنسان ضمن الحلقة وحفظ المحادثات.
import { Agent, BedrockModels } from '@aws-blocks/blocks';
const assistant = new Agent(scope, 'assistant', {
model: { deployed: BedrockModels.BALANCED },
systemPrompt:
'أنت تساعد المستخدمين على إدارة قائمة مهامهم. كن موجزًا. ' +
'استخدم أداة listOpenTasks قبل الإجابة عن الأسئلة المتعلقة بحجم العمل.',
streamingMode: 'token',
tools: (tool) => ({
listOpenTasks: tool({
description: 'سرد المهام المفتوحة للمستخدم الحالي',
parameters: z.object({ userId: z.string() }),
execute: async ({ userId }) =>
Array.fromAsync(
tasks.query({
where: { userId: { equals: userId } },
})
),
}),
}),
});يقابل BedrockModels.BALANCED حاليًا نموذج Claude Sonnet 4.6 وهو الخيار الافتراضي الموصى به؛ بينما يقابل BedrockModels.SMART نموذج Claude Opus 4.8 للمهام الأصعب. وهذه إعدادات مسبقة مسمّاة بحسب القدرة، فيمكن ترقية النموذج الكامن دون تغيير شيفرتك. وهي تستخدم ملفات استدلال عالمية، ما يعني أن الطلبات قد تُوجَّه إلى أي منطقة مدعومة — فإن كانت لديك متطلبات لإقامة البيانات، فحدّد صراحة ملف استدلال مقيَّدًا بمنطقة بعينها.
محليًا، يستخدم الوكيل مزوّدًا محاكى قائمًا على الكلمات المفتاحية: ردود متوقعة، بلا مفتاح API، وبلا تكلفة، وبلا شبكة. وهذا يجعل شيفرة الوكيل قابلة للاختبار في التكامل المستمر. ولاختبار نموذج حقيقي محليًا، وجّهه إلى Ollama أو أي نقطة نهاية متوافقة مع OpenAI:
model: {
deployed: BedrockModels.BALANCED,
local: {
provider: 'openai-api',
modelId: 'llama3.1:8b',
endpoint: 'http://localhost:11434/v1',
apiKey: 'ollama',
},
},البث التدريجي بالشكل الصحيح
تُرسل stream() الرسالة عبر AsyncJob وتعود فورًا — فلا خطر من انتهاء مهلة API Gateway في جلسات الوكيل الطويلة — ثم تنشر الأجزاء على قناة Realtime. اشترك قبل أن ترسل، وإلا فقدت أول الرموز:
const conversationId = await assistant.createConversationId(userId);
const channel = await assistant.getChannel(conversationId);
const sub = channel.subscribe((chunk) => {
if (chunk.type === 'text-delta') appendToUI(chunk.text);
if (chunk.type === 'tool-call') showSpinner(chunk.toolName);
if (chunk.type === 'done') finish(chunk.text, chunk.usage);
});
await sub.established;
const result = await assistant.stream('على ماذا ينبغي أن أعمل اليوم؟', {
conversationId,
userId,
});
const done = await result.complete();الـ Block المسمى Agent لا يتحقق من صلاحيات القراءة. فالطريقتان getConversation(id) وgetPendingInterrupts(id) لا تأخذان سوى معرّف، وأي مستدعٍ يملك معرّف محادثة صالحًا يحصل على الرسائل. التخويل مسؤوليتك أنت داخل معالج الواجهة البرمجية: اشتق userId من الجلسة، واستدعِ listConversations(userId)، وتأكد أن المحادثة تخص ذلك المستخدم قبل إعادة أي شيء. أما deleteConversation(id, userId) فمقيَّدة بالمالك داخليًا وهي آمنة.
الخطوة 8: النشر على AWS
الآن، والآن فقط، تحتاج إلى حساب AWS.
إعداد لمرة واحدة — اضبط AWS CLI، وتحقق منها، وهيّئ CDK بـ bootstrap لحسابك ومنطقتك:
aws sts get-caller-identity
npx cdk bootstrap aws://123456789012/eu-west-1التهيئة مطلوبة مرة واحدة فقط لكل زوج من الحساب والمنطقة.
ثم انشر إلى بيئة اختبار — بيئة سريعة ومؤقتة خاصة بكل مطوّر، تستخدم التبديل الساخن لدوال Lambda بدل تحديثات CloudFormation الكاملة:
npm run sandboxيستغرق هذا ثوانٍ لا دقائق، وتتحول الآن كل الـ Blocks إلى خدمات AWS حقيقية: جداول DynamoDB، ونقطة نهاية API Gateway، ودالة Lambda، وطابور SQS، وجدولة EventBridge، وحاوية S3. وشيفرة تطبيقك مطابقة بايتًا ببايت لما كان يعمل محليًا.
تهم بيئات الاختبار لأن التطبيقات المحلية أمينة لكنها ليست مثالية. ومن الأمور الجديرة بالاختبار على الخدمات الحقيقية: أداء استعلامات DynamoDB على أحجام بيانات واقعية، وحدود صلاحيات IAM، وسلوك CORS في S3 من أصل متصفح حقيقي، ومخرجات نموذج Bedrock الفعلية مقارنةً بالمزوّد المحاكى.
للبيئة التجريبية أو الإنتاج، شغّل نشر CloudFormation كاملًا:
npm run deployأوامر الإزالة:
npm run sandbox:destroy # إزالة بيئة الاختبار المؤقتة
npm run destroy # إزالة النشر الكاملالخطوة 9: المخرج إلى CDK عند الحاجة
عادةً ما تفشل أطر البنية التحتية المشتقة من الشيفرة عند الحدود: فما إن تحتاج موردًا واحدًا لا ينمذجه الإطار حتى تجد نفسك عالقًا. يعالج AWS Blocks ذلك بـطبقة CDK اختيارية في aws-blocks/index.cdk.ts. وإن لم تنشئ الملف قط، فسيُولَّد لك واحد.
// aws-blocks/index.cdk.ts
import * as cdk from 'aws-cdk-lib';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { BlocksStack } from '@aws-blocks/blocks/cdk';
const app = new cdk.App();
const stack = await BlocksStack.create(app, 'task-tracker-stack', {
backendHandlerPath: './index.handler.ts',
backendCDKPath: './index.ts',
});
const queue = new sqs.Queue(stack, 'legacy-queue');
queue.grantSendMessages(stack.handler);
stack.handler.addEnvironment('QUEUE_URL', queue.queueUrl);تحصل على كائن Stack الخام من CDK وعلى دالة الخلفية عبر stack.handler، فيصبح أي construct في منظومة CDK متاحًا — نطاقات مخصصة، وإعدادات VPC، ومواضيع SNS، وموارد قائمة. لا يوجد جدار تصطدم به.
وينطبق المبدأ نفسه على تبنّي بنية تحتية قائمة. فالدوال KVStore.fromExisting(tableName) وDistributedTable.fromExisting(tableName) وFileBucket.fromExisting(bucketName) تغلّف موارد تملكها بالفعل بدل إنشاء أخرى جديدة — فيمكنك وضع واجهة Blocks أمام جدول DynamoDB إنتاجي دون ترحيل أي شيء.
اختبار ما بنيته
تحقق محليًا، بهذا الترتيب:
- حدود المصادقة. استدعِ
api.listTasks()قبل تسجيل الدخول. ينبغي أن تحصل علىSessionExpiredExceptionبرمز 401، لا على مصفوفة فارغة. فإن حصلت على مصفوفة فارغة، فثمة معالج ينقصهrequireAuth. - عزل المستأجرين. سجّل مستخدمَين، وأنشئ مهامًّا بكل منهما، وتأكد أن أيًّا منهما لا يرى صفوف الآخر.
- التحقق من المخطط. استدعِ
api.createTask('')وتأكد أن قيد Zod المسمىmin(1)يرفضه. فالتحقق يجري على الخادم لا في المتصفح وحده. - التوزيع الفوري. افتح تبويبين بالمستخدم نفسه. إنشاء مهمة في أحدهما ينبغي أن يظهر في الآخر دون تحديث الصفحة.
- أمان الأنواع. أضف معاملًا إلى طريقة في الخلفية وتأكد أن واجهتك تفشل في الترجمة قبل تشغيلها. هذه هي الخاصية التي وُجد الإطار كله من أجلها.
- الاستمرارية. أعد تشغيل
npm run dev. تبقى بياناتKVStoreوFileBucketفي.bb-data/؛ أماDistributedTableفهو في الذاكرة محليًا ولا يبقى.
حل المشكلات
tasks.put is not a function — استدعيت طريقة بيانات في المستوى الأعلى من aws-blocks/index.ts. تُنفَّذ تلك الشيفرة أثناء توليف CDK حيث يكون الـ Block construct بنية تحتية بلا طرق بيانات. انقل الاستدعاء إلى داخل طريقة في الواجهة البرمجية أو معالج مهمة أو سكربت تشغيل.
get() يطالب بكل حقول نوعي — مرّرت وسيط نوع صريحًا واحدًا: new DistributedTable<Task>(...). احذفه ودع الاستنتاج يقوم بعمله، أو وفّر الأنواع العامة الثلاثة.
ZodType missing properties — يتطلب الـ Block المسمى Agent إصدار Zod 4 كاعتمادية نظيرة. ابحث عن نسخة Zod مكررة أو أقدم في ملف القفل.
رموز الوكيل الأولى مفقودة — استدعيت stream() قبل الاشتراك. اشترك، وانتظر sub.established، ثم أرسل.
أُعيد إنشاء المورد وضاعت البيانات بعد النشر — تغيّر معرّف Block. معرّفات الـ Blocks هي هوية المورد؛ وتغيير أحدها يساوي حذفًا وإعادة إنشاء. استعِد من نسخة احتياطية وأعد المعرّف السابق.
أخطاء في إصدار Node — يتطلب AWS Blocks الإصدار 22 فأحدث. شغّل nvm use 22.
AWS Blocks في مواجهة Amplify وSST وEncore
Blocks ليس Amplify Gen 2 بحلة جديدة. فـ Amplify منصة استضافة وخلفية بلوحة تحكم؛ أما Blocks فمكتبة تولّد CDK، بلا لوحة تحكم وبلا مستوى تحكم مُدار — بل إن أداة سطر الأوامر تتكامل صراحةً مع مشروع Amplify Gen 2 قائم بدل أن تستبدله.
بالمقارنة مع SST Ion، يقايض Blocks الاتساع بقصة محلية أمتن بكثير: فوضع dev في SST يمرّر الطلبات إلى موارد AWS منشورة فعليًا، بينما يشغّل Blocks تطبيقات محلية حقيقية دون أي حساب سحابي. وبالمقارنة مع Encore.ts، تتقارب فلسفة الـ RPC والبنية التحتية المستنتَجة، لكن Blocks يخرج CDK قياسيًا يمكنك قراءته وتوسيعه ثم مغادرته يومًا ما. وبالمقارنة مع Alchemy، يجيب الاثنان عن السؤال نفسه على سحابتين مختلفتين.
الإطار الذي اختارته AWS عند الإطلاق هو أن Blocks مصمَّم كي تنتج وكلاء البرمجة بالذكاء الاصطناعي خلفيات صحيحة من المحاولة الأولى: فمساحة الواجهة صغيرة، والتسمية لا تحتمل اللبس، ولا سبيل لكتابة دالة Lambda تفتقر إلى صلاحية قراءة جدولها الخاص. وسواء كتبت شيفرتك بمساعدة وكيل أم لا، فإن هذا القيد ينتج إطارًا مريحًا للبشر أيضًا.
الخطوات التالية
- استبدل
AuthBasicبـAuthCognitoقبل الإنتاج — فستحصل على المصادقة متعددة العوامل وSAML وتسجيل الدخول الاجتماعي ومفاتيح المرور، علمًا أنAuthBasicمحصور صراحةً بالنماذج الأولية والأدوات الداخلية. - استبدل
DistributedTableبـDatabaseإن كانت أنماط وصولك تحتاج عمليات JOIN. فهو يعمل بـ PGlite محليًا وAurora Serverless v2 على AWS، مع Kysely لكتابة SQL بأنواع محددة. - أضف
KnowledgeBaseإلى جانبAgentلبناء خط أنابيب RAG فوق مستنداتك الخاصة. - اربط
LoggerوMetricsوTracer، ثم أضفDashboardلتوليد لوحة CloudWatch تلقائيًا من تعريفات مقاييسك. - ولّد عملاء أصليين. فملف
blocks.spec.jsonينتج عملاء بأنواع محددة بلغات Kotlin Multiplatform وSwift وDart تستدعي الخلفية نفسها عبر JSON-RPC. - قراءات ذات صلة: بناء خوادم MCP بلغة TypeScript، وCloudflare Workflows للتنفيذ الدائم، وسير عمل Temporal الدائم.
الخلاصة
AWS Blocks هو أول إطار لاشتقاق البنية التحتية من الشيفرة يصدر عن AWS نفسها ولا يبدو فخًّا. فالقصة المحلية محلية فعلًا — بلا حساب ولا بيانات اعتماد ولا شبكة — وهذا يغيّر طريقة انضمام المطورين الجدد إلى الفريق وطريقة عمل التكامل المستمر. وأمان الأنواع قائم على استنتاج حقيقي من طرف إلى طرف، لا على توليد شيفرة عليك أن تتذكر إعادة تشغيله. ومخرج CDK يعني أن آراء الإطار نقطة انطلاق لا سقفًا.
تحفظات المعاينة حقيقية: معرّفات الـ Blocks لا تسامح، وAuthBasic ليس مصادقة إنتاجية، والـ Block المسمى Agent يترك لك تخويل القراءات، وستتغير مساحة الواجهة قبل الإصدار العام. لكن الرهان الجوهري — أن سطر الشيفرة الذي ينشئ جدولًا ينبغي أن يكون الجدول — هو ما تحوم حوله AWS منذ عقد، وهذه أقرب محاولة إلى تحقيقه حتى الآن.
ابدأ بـ npm create @aws-blocks/blocks-app@latest. ستحصل على خلفية موثّقة تعمل قبل أن تنتهي من تقرير ما إذا كانت تعجبك.