تخطَّ إلى المحتوى

أين يسكن تكامل Xero داخل تطبيق Next.js

نداء العودة في OAuth مكانه Route Handler. ولا شيء آخر تقريبًا. خريطة لما يصمد من تكامل محاسبي أمام بيئة بلا خوادم وما لا يصمد بصمت.

6 دقيقة قراءة

التكامل المحاسبي المبني داخل تطبيق Next.js يعمل عادةً من أول محاولة في التطوير ثم يتصرف على نحو غريب في الإنتاج، بطرق لا علاقة لها بالنظام المحاسبي. النداءات صحيحة. وبيانات الاعتماد صحيحة. المختلف هو كم نسخة من التطبيق موجودة، وكم يُسمح لكل منها أن تعيش.

ويستحق الأمر رسم الخريطة قبل كتابة أي منه، لأن القطع التي تنتمي إلى تطبيق Next.js وتلك التي لا تنتمي يسهل تمييزها متى عرفت ما تبحث عنه، ويصعب جدًا فصلها بعد ذلك.

الجزء الذي ينتمي إلى هنا فعلًا

العودة من شاشة الموافقة هي Route Handler، وهي القطعة الوحيدة من التكامل التي يكون Next.js موطنها الطبيعي.

// app/api/xero/callback/route.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  const params = new URL(request.url).searchParams
  const jar = await cookies()
  const expected = jar.get('xero_state')?.value
 
  if (!expected || params.get('state') !== expected) {
    redirect('/settings/accounting?error=state')
  }
 
  jar.delete('xero_state')
  await exchangeCodeForTokens(params.get('code')!)
  redirect('/settings/accounting?connected=1')
}

وهو قصير لأنه يجب أن يكون كذلك. قيمة state تخرج في كعكة httpOnly حين ترسل المستخدم إلى شاشة الموافقة وتُقارَن في طريق العودة - وبغير هذه المقارنة ستقبل النقطة رمزًا من أي أحد يستطيع دفع متصفح لزيارتها. يُبادَل الرمز على الخادم، ولا تدخل الرموز قط في حمولة يستطيع مكوّن عميل قراءتها، والمعالج يحوّل بدل أن يعرض شيئًا.

ورابط الإذن يُبنى في Server Action أو مكوّن خادم، للسبب نفسه: معرّف العميل ليس سرًا، لكن المسار أسهل في التفكير حين يبنيه طرف واحد من التطبيق فقط.

هذا هو الحد. وكل ما تحت هذا الخط هو حيث يتوقف Next.js عن المساعدة.

نطاق الوحدة غير مشترك، والرموز ليست ثابتة

التحسين البديهي هو الاحتفاظ برمز الوصول في متغير على مستوى الوحدة كي لا تقرأ قاعدة البيانات مع كل عرض.

let cachedToken: string | null = null   // لا تفعل

في التطوير هذا صحيح وسريع. وبعد النشر يكون لكل نسخة من الدالة نطاق وحدة خاص بها، فذلك المتغير ليس ذاكرة واحدة بل بعدد ما تُبقيه المنصة دافئًا من نسخ. وهي الحقيقة البنيوية نفسها التي تجعل تجميع الاتصالات يتصرف على غير المتوقع

  • ذاكرة منفصلة لكل نسخة - لكن النتيجة هنا أسوأ من مجمّع استُنفد.

ورمز التحديث في Xero يُستعمل مرة واحدة. فالتجديد يمنحك جديدًا ويسحب الذي أرسلته. فإن لاحظت نسختان استقلالًا أن الرمز انتهى وجدّدت كل منهما، فهما لا تكرران طلبًا بريئًا. إنهما تتسابقان على بيانات اعتماد لا يملكها في النهاية إلا واحدة، والكتابة الخاسرة قد تترك رمزًا ميتًا في قاعدة بياناتك دون أن يظهر خطأ في أي مكان.

والعلاج ليس ذاكرة أفضل. العلاج أن يقع التجديد في مكان واحد بالضبط، خلف قفل يعيش خارج العملية - قفل صف في قاعدة بياناتك، أو مفتاح في Redis بصلاحية قصيرة. كل نسخة تقرأ الرمز من المخزن، وإن كان منتهيًا انتظرت من يحمل القفل بدل أن تجدّد لنفسها.

وهو ما يثير سؤال أين ينبغي أن تعمل هذه الشيفرة، والجواب أنها ينبغي ألا تكون Route Handler أصلًا. طلب الويب مكان خاطئ للإمساك بقفل.

نقطة الـ webhook والبدء البارد

الـ Route Handler هو الشكل الصحيح لاستقبال webhook، وما عليه فعله هو الرد فورًا.

يوقّع Xero تسليماته بـ HMAC على الجسم الخام، فاقرأ الجسم نصًا واحسب تجزئة ذلك النص - وأي شيء يحلّله ويعيد تسلسله قبل ذلك يُنتج سلسلة مختلفة وفشل توقيع يشبه تمامًا مفتاحًا خاطئًا. ويُفعَّل الاشتراك كذلك بنداء تحقق يجب الرد عليه صحيحًا قبل أن يصل أي حدث حقيقي.

والتسليم لا ينتظر طويلًا. والمنصة كذلك، والبدء البارد يُستهلك قبل أن تعمل شيفرتك أصلًا. فيتحقق المعالج، ويسجّل الإشعار في مكان دائم، ويعود:

export async function POST(request: Request) {
  const raw = await request.text()
  if (!verify(raw, request.headers.get('x-xero-signature'))) {
    return new Response(null, { status: 401 })
  }
 
  await enqueue(JSON.parse(raw).events)   // دائم، خارج هذه العملية
  return new Response(null, { status: 200 })
}

وafter مغرية هنا وليست الأداة الصحيحة. إنها تشغّل الدالة بعد إرسال الرد، لكنها تبقى داخل الاستدعاء نفسه وداخل المدة القصوى نفسها للمسار، ما يعني أنها لا تُعاد محاولتها، ولا تتجاوز الطلب، ولا ضمان أنها انتهت إن اختفت النسخة. للتسجيل هذا مقبول. أما للنسخة الوحيدة من حدث لن يُرسَل إليك غيرها فلا.

وما تشير إليه enqueue هو حقًا خارج التطبيق: طابور، أو سير عمل دائم، أو صف في جدول مهام يستعلم عنه عامل. حمولة الـ webhook تحمل معرّفًا لا الفاتورة، فلا بد لشيء أن يذهب ويجلب السجل بعدها - وذلك الجلب خاضع لحدّ طلبات، فيلزم أن يقدر على التراجع وإعادة المحاولة لاحقًا. ولا كلمة من هذه تصف Route Handler.

ماذا ينبغي أن تقرأ الصفحات

بعد الحصول على البيانات، يأتي الإغراء بنداء Xero من مكوّن خادم كي تعرض الصفحة أرقامًا حيّة. قاوم ذلك، والسبب ليس استراتيجية التخزين المؤقت.

كل عرض لتلك الصفحة يصير نداءً على حدّ خاص بالمستأجر تتقاسمه مع عملية المزامنة لديك. وكل عرض يرث زمن استجابة النظام الآخر ونوافذ صيانته. و429 أثناء العرض صفحة تفشل، لا رقم تأخّر قليلًا.

فمكوّنات الخادم تقرأ قاعدة بياناتك أنت، وهي سريعة ومتاحة دائمًا ولك أن تفهرسها. وكيف تخزّنها مؤقتًا يصير عندئذ قرارًا عاديًا في بياناتك بدل تفاوض مع واجهة غيرك.

وسؤال التصميم الوحيد الباقي هو الوسم الصادق. الأرقام على الشاشة نسخة، وللنسخة عمر. خزّن ختم آخر مزامنة ناجحة بجوار السجلات واعرضه - "حتى 14:20" يكلّف سطرًا ويمنع صنف البلاغات الذي ينظر فيه أحدهم إلى رقم يظنه آنيًا. وحين تكون المزامنة متعثرة، قل ذلك على الصفحة لا في قناة تنبيهات فقط.

الشكل الذي ينجح

ضع نداء العودة، ومستقبل الـ webhook، ومسار القراءة داخل تطبيق Next.js. وضع تجديد الرموز، والسحب المجدول، ومنطق إعادة المحاولة، والمطابقة في شيء يعمل بجدوله الخاص ويصمد أمام النشر. وذلك الشيء الثاني خدمة صغيرة، أو عامل طابور، أو إطار خلفية يحمل ذلك كله أصلًا.

هذا ليس قيدًا يُلتف حوله، وتطبيق Next.js الذي يحادث نظامًا خارجيًا بهذه الطريقة يحادث الخامس بالطريقة نفسها. وهي الخلاصة ذاتها في مسألة متى لا تلجأ إلى Next.js: حين تكون الصعوبة في المهام لا في مسار العرض، تكون الواجهة الأمامية القرار الثاني لا الأول.

وإن كان النظام المحاسبي هو Sage بدلًا من ذلك، تتغير الخريطة في نقطة مهمة، لأن عدة منتجات من Sage ليس لها على الإنترنت ما تناديه، وينتقل التكامل كله خلف حدّ لا يرى تطبيقك ما وراءه.

أسئلة ذات صلة

هل يمكن أن يكون التكامل كله Route Handlers؟
يمكنك كتابته هكذا وسيمرّ في المراجعة. لكنه لن يصمد أمام انقطاع في الطرف الآخر، لأن لا شيء في هذا النموذج يعيد المحاولة. الـ Route Handler يعمل حين يصل طلب ويتوقف حين يخرج الرد، فأي عمل يجب أن يقع لاحقًا لا مكان له ليعمل فيه.
هل تمنحنا after() مهمة في الخلفية؟
تمنحك عملًا يجري بعد إرسال الرد، وهذا ليس الشيء نفسه. فالدالة لا تزال تعمل داخل الاستدعاء نفسه وداخل المدة القصوى نفسها للمسار، فلا تستطيع تجاوزه ولا إعادة محاولتها بمعزل عنه. هي الأداة الصحيحة للتسجيل، والخاطئة لأي شيء يزعجك فقدانه.
أين تُخزَّن الرموز؟
في قاعدة بياناتك أنت، مشفّرة، ومفتاحها المستأجر الذي تخصّه. لا في نطاق الوحدة، لأن لكل نسخة من الدالة نسخة منفصلة منه. ولا في كعكة، لأن عملية في الخلفية لا طلب لديها لتقرأ منه. ولا في متغير بيئة، لأنه يتغير كل نصف ساعة.
هل الاستضافة الذاتية مخرج من كل هذا؟
تزيل مشكلة عدد النسخ، لأن عملية Node طويلة العمر لها نطاق وحدة واحد وذاكرة واحدة. ولا تزيل الحاجة إلى إعادة محاولة دائمة وعمل مجدول، وبناء ذلك داخل عملية الويب هو كيف يتحول نشرٌ إلى مزامنة ضائعة. نموذج الاستضافة يغيّر أي المشكلات لديك لا عددها.

العودة إلى كل المقالات