لقد بنيت ثلاث أنظمة مزاد خلال السنتين الماضيتين. الأولى كانت فوضى — استقصاء قاعدة البيانات كل ثانية، شروط تنافسية في كل مكان، عروض تختفي في العدم. الثانية كانت أفضل لكنها تطلبت إدارة خادم WebSocket منفصل إلى جانب API الرئيسي. الثالثة؟ هذه هي التي سأوضحها لك. تستخدم Supabase Realtime، وهي المرة الأولى التي شعرت فيها أن بناء محرك المزاد شعر به بشكل صحيح.

يقع Supabase Realtime في الأعلى من سجل الكتابة المسبقة لـ PostgreSQL (WAL) ويستخدم خادم قائم على Elixir لدفع تغييرات قاعدة البيانات عبر WebSockets إلى العملاء المتصلين. بالنسبة لنظام المزاد، هذا يعني أن كل عرض يصل إلى قاعدة البيانات ينتشر على الفور إلى كل مزايد يراقب هذا المزاد. لا استقصاء. لا بنية pub/sub منفصلة. قاعدة البيانات الخاصة بك هي نظام الأحداث الخاص بك.

دعونا نبني واحدة من الصفر.

جدول المحتويات

نظرة عامة على العمارة

قبل كتابة أي كود، دعنا نفهم ما نبنيه وكيف تتناسب الأجزاء معاً.

يعطيك Supabase Realtime ثلاث بدائيات تتطابق بشكل مثالي مع متطلبات المزاد:

  • تغييرات Postgres: الاشتراك في أحداث INSERT و UPDATE و DELETE على جداول العروض والمزادات الخاصة بك. عندما يضع شخص ما عرض، كل مشترك يحصل على بيانات الصف الجديدة في غضون ميلي ثواني.
  • البث: إرسال رسائل مؤقتة إلى مشاركي القناة. مثالي لإخطارات "لقد تم تجاوزك في العرض" التي لا تحتاج إلى استمرار.
  • الحضور: تتبع من يراقب المزاد حالياً. هذا يتيح لك عرض "14 مزايد يراقبون" في واجهة المستخدم الخاصة بك واكتشاف الجلسات الوهمية.

تبدو تدفق البيانات هكذا:

  1. يقدم المزايد عرض من خلال الواجهة الأمامية الخاصة بك
  2. استدعاء RPC أو إدراج مباشر يصل إلى جدول bids الخاص بك
  3. مشغل PostgreSQL يتحقق من صحة مبلغ العرض ويحدث auctions.current_high_bid
  4. يختار Supabase Realtime تغيير WAL ويدفعه إلى جميع المشتركين على قناة المزاد الخاصة بك
  5. ينطلق مشغل ثاني حدث بث لإخطار المزايد السابق الأعلى بأنهم تم تجاوزهم في العرض
  6. يحدث كل عميل متصل واجهة المستخدم الخاصة به في الوقت الفعلي

زمن الكمون من وضع العرض إلى تحديث واجهة المستخدم عبر جميع العملاء عادة أقل من 100 ميلي ثانية. قمت بقياس p99 حول 80-90 ميلي ثانية في الإنتاج على طبقة Supabase Pro.

لماذا لا تستخدم الاستقصاء فقط؟

أعلم أن بعضكم يفكر "ألا يمكنني الاستقصاء كل 500 ميلي ثانية؟" يمكنك. لكن مع 200 مزايد متزامن على مزاد واحد، هذا 400 طلب في الثانية تضرب قاعدة البيانات الخاصة بك لمزاد واحد. اضرب هذا في 50 مزاد نشط وأنت على 20000 استعلام في الثانية — معظمها لا يُرجع شيء جديد. تقلب WebSockets هذا النموذج: صفر استعلامات عندما لا يتغير شيء، تحديثات فورية عندما يتغير شيء ما.

مخطط قاعدة البيانات والإعداد

إليك المخطط الذي أستخدمه. إنه بسيط عن قصد — يمكنك توسيع نطاقه، لكن الهيكل الأساسي يتعامل مع معظم أنواع المزادات.

-- جدول المزادات
CREATE TABLE auctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_name TEXT NOT NULL,
  description TEXT,
  starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
  current_high_bid DECIMAL(12,2) DEFAULT 0,
  highest_bidder_id UUID REFERENCES auth.users(id),
  min_increment DECIMAL(12,2) DEFAULT 1.00,
  status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
  starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- جدول العروض
CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  amount DECIMAL(12,2) NOT NULL,
  placed_at TIMESTAMPTZ DEFAULT NOW(),
  CONSTRAINT positive_amount CHECK (amount > 0)
);

-- فهرس للبحث السريع عن العروض لكل مزاد
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);

-- حرج: تفعيل هوية النسخ المتماثلة للبث الحي
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

إعداد REPLICA IDENTITY FULL ضروري. بدونها، يحصل Supabase Realtime فقط على المفتاح الأساسي على أحداث UPDATE و DELETE — وليس بيانات الصف الكاملة. بالنسبة لنظام المزاد، تحتاج إلى الحمل الكامل حتى يتمكن العملاء من تحديث مبالغ العروض بدون إجراء استعلام منفصل.

تفعيل نسخ Realtime

في لوحة Supabase Dashboard، انتقل إلى Database → Replication وقم بتفعيل النسخ المتماثلة لكلا جداول auctions و bids. بدلاً من ذلك، يمكنك القيام بذلك باستخدام SQL:

BEGIN;
  -- إزالة المنشور الموجود إن وجد
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- إنشاء منشور بكلا الجدولين
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

أمان على مستوى الصف

لا تتخطى هذا. RLS هي طبقة التحقق من الصحة من جانب الخادم.

ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;

-- يمكن لأي شخص عرض المزادات النشطة
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- يمكن للمستخدمين المصرح لهم عرض جميع العروض على المزادات النشطة
CREATE POLICY "View bids on active auctions" ON bids
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = bids.auction_id
      AND auctions.status = 'active'
    )
  );

-- فقط المستخدمون المصرح لهم يمكنهم تقديم العروض
CREATE POLICY "Place bids" ON bids
  FOR INSERT WITH CHECK (
    auth.uid() = user_id
    AND EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = auction_id
      AND auctions.status = 'active'
      AND auctions.ends_at > NOW()
    )
  );

مشغلات PostgreSQL لمنطق العرض

هنا يحدث السحر الحقيقي. تفرض قاعدة البيانات كل منطق العرض من جانب الخادم — لا يمكن للعميل الغش.

مشغل التحقق من صحة العرض وتحديث المزاد

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- قفل صف المزاد لمنع الشروط التنافسية
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- التحقق من أن المزاد نشط
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- التحقق من أن المزاد لم ينته
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- التحقق من أن مبلغ العرض يتجاوز الحد الأعلى الحالي + الحد الأدنى للزيادة
  IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
    RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
      v_auction.min_increment, v_auction.current_high_bid;
  END IF;

  -- منع المزايد من تجاوز عرضه الخاص
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- تحديث المزاد برقم العرض الجديد الأعلى
  UPDATE auctions
  SET
    current_high_bid = NEW.amount,
    highest_bidder_id = NEW.user_id,
    updated_at = NOW()
  WHERE id = NEW.auction_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER validate_and_process_bid
  BEFORE INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION process_new_bid();

هذا القفل FOR UPDATE على صف المزاد حرج. بدونه، قد يصل عرضان في نفس الوقت ويقرآ نفس current_high_bid، كلاهما يمر التحقق، والاثنان يحصلان على إدراج. القفل يسلسل الوصول.

بث إخطارات تجاوز العرض

ينطلق هذا المشغل بعد عرض ناجح ويرسل إخطار مؤقت إلى قناة المزاد:

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- ابحث عمن تم تجاوزه للتو
  SELECT user_id INTO v_previous_bidder
  FROM bids
  WHERE auction_id = NEW.auction_id
    AND id != NEW.id
  ORDER BY amount DESC
  LIMIT 1;

  -- بث إخطار تجاوز العرض إذا كان هناك مزايد سابق
  IF v_previous_bidder IS NOT NULL THEN
    PERFORM realtime.send(
      jsonb_build_object(
        'auction_id', NEW.auction_id,
        'new_high', NEW.amount,
        'outbid_user', v_previous_bidder,
        'new_leader', NEW.user_id
      ),
      'outbid',
      'auction:' || NEW.auction_id::text,
      true
    );
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER after_bid_notify
  AFTER INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION notify_outbid();

الاشتراك من جانب العميل باستخدام JavaScript

الآن دعونا نربط الواجهة الأمامية. سأعرض هذا باستخدام أنماط JavaScript/React الفانيلا — نفس النهج يعمل إذا كنت تبني باستخدام Next.js أو أي إطار عمل آخر.

تهيئة العميل

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    realtime: {
      params: {
        eventsPerSecond: 20 // خنق لحركة مرور المزاد
      }
    }
  }
);

معامل eventsPerSecond مهم. على مزاد ساخن به عشرات العروض في الثانية، لا تريد إعادة تقديم 50 مرة في الثانية. عشرين تحديث في الثانية أكثر من كافي لواجهة مستخدم سلسة.

الاشتراك في قناة المزاد

function subscribeToAuction(auctionId, callbacks) {
  const channel = supabase.channel(`auction:${auctionId}`);

  channel
    // الاستماع للعروض الجديدة عبر تغييرات Postgres
    .on('postgres_changes', {
      event: 'INSERT',
      schema: 'public',
      table: 'bids',
      filter: `auction_id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onNewBid(payload.new);
    })

    // الاستماع لتغييرات حالة المزاد
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // الاستماع لإخطارات البث بتجاوز العرض
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // تتبع المزايدين النشطين عبر الحضور
    .on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState();
      const bidderCount = Object.keys(state).length;
      callbacks.onPresenceUpdate(bidderCount, state);
    })

    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        // تتبع حضور هذا المستخدم
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

React Hook لاشتراكات المزاد

import { useState, useEffect, useCallback } from 'react';

function useAuction(auctionId) {
  const [auction, setAuction] = useState(null);
  const [bids, setBids] = useState([]);
  const [bidderCount, setBidderCount] = useState(0);
  const [isOutbid, setIsOutbid] = useState(false);

  useEffect(() => {
    // جلب الحالة الأولية
    async function loadAuction() {
      const { data: auctionData } = await supabase
        .from('auctions')
        .select('*')
        .eq('id', auctionId)
        .single();
      setAuction(auctionData);

      const { data: bidData } = await supabase
        .from('bids')
        .select('*')
        .eq('auction_id', auctionId)
        .order('amount', { ascending: false })
        .limit(20);
      setBids(bidData || []);
    }
    loadAuction();

    // الاشتراك في التحديثات الحية
    const channel = subscribeToAuction(auctionId, {
      onNewBid: (bid) => {
        setBids(prev => [bid, ...prev].slice(0, 20));
        setIsOutbid(false);
      },
      onAuctionUpdate: (updated) => setAuction(updated),
      onOutbid: (payload) => {
        const currentUser = supabase.auth.getUser()?.data?.user;
        if (payload.outbid_user === currentUser?.id) {
          setIsOutbid(true);
        }
      },
      onPresenceUpdate: (count) => setBidderCount(count)
    });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [auctionId]);

  const placeBid = useCallback(async (amount) => {
    const user = (await supabase.auth.getUser()).data.user;
    const { data, error } = await supabase
      .from('bids')
      .insert({
        auction_id: auctionId,
        amount: parseFloat(amount),
        user_id: user.id
      })
      .select()
      .single();

    if (error) throw new Error(error.message);
    return data;
  }, [auctionId]);

  return { auction, bids, bidderCount, isOutbid, placeBid };
}

التعامل مع الشروط التنافسية والتحقق من صحة العرض

الشروط التنافسية هي أكبر مصدر واحد للأخطاء في أنظمة المزادات. إليك كيفية تعاملي معها.

من جانب الخادم: PostgreSQL يقوم بالعمل الثقيل

SELECT ... FOR UPDATE في دالة المشغل لدينا هو السطر الأول من الدفاع. لكن هناك نمط آخر بدأت باستخدامه — أقفال استشارية للمزادات عالية التنافس:

CREATE OR REPLACE FUNCTION place_bid_safe(
  p_auction_id UUID,
  p_user_id UUID,
  p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
  v_lock_key BIGINT;
  v_bid_id UUID;
BEGIN
  -- توليد مفتاح قفل حتمي من UUID المزاد
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- الحصول على قفل استشاري (يمنع العروض المتزامنة على نفس المزاد)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- الآن آمن للإدراج (المشغل يتعامل مع التحقق)
  INSERT INTO bids (auction_id, user_id, amount)
  VALUES (p_auction_id, p_user_id, p_amount)
  RETURNING id INTO v_bid_id;

  RETURN QUERY
  SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

استدعِ هذا من العميل باستخدام RPC الخاص بـ Supabase:

const { data, error } = await supabase.rpc('place_bid_safe', {
  p_auction_id: auctionId,
  p_user_id: user.id,
  p_amount: bidAmount
});

من جانب العميل: واجهة مستخدم متفائلة مع التراجع

اعرض العرض على الفور في واجهة المستخدم، لكن كن مستعداً للتراجع عنه إذا رفضه الخادم:

async function handleBidSubmit(amount) {
  const optimisticBid = {
    id: crypto.randomUUID(),
    amount,
    user_id: user.id,
    placed_at: new Date().toISOString(),
    _optimistic: true
  };

  // اعرض على الفور
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // سيصل العرض الحقيقي عبر Realtime ويحل محل العرض المتفائل
  } catch (err) {
    // إزالة العرض المتفائل عند الفشل
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

تتبع الحضور للمزايدين النشطين

يعرض عدد الأشخاص الذين يراقبون المزاد إحساساً بالإلحاح. تتبع الحضور بسيط جداً مع Supabase:

// تحديث حالة المستخدم عند بدء المزايدة
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

على جانب العرض، يمكنك تقسيم حالة الحضور لعرض عدد من يقومون بالمزايدة بنشاط مقابل من يراقب فقط:

function parseBidderStats(presenceState) {
  const users = Object.values(presenceState).flat();
  return {
    total: users.length,
    bidding: users.filter(u => u.status === 'bidding').length,
    watching: users.filter(u => u.status === 'watching').length
  };
}

ضبط الأداء والاعتبارات الإنتاجية

الخنق والإرجاء

حرب المزايدة يمكن أن تولد عشرات الأحداث في الثانية. إليك ما أقوم بتكوينه:

  • من جانب الخادم: eventsPerSecond: 20 على تكوين عميل Supabase
  • من جانب العميل: أرجِ زر العرض عند 300 ميلي ثانية لمنع النقرات المزدوجة
  • تحديثات واجهة المستخدم: استخدم requestAnimationFrame لتحريكات قائمة العروض

توقيت انتهاء المزاد

لا تثق في ساعة العميل. استخدم وظيفة PostgreSQL cron عبر pg_cron:

-- تشغيل كل 10 ثوان لإغلاق المزادات المنتهية
SELECT cron.schedule(
  'close-expired-auctions',
  '*/10 * * * * *',
  $$
  UPDATE auctions
  SET status = CASE
    WHEN highest_bidder_id IS NOT NULL THEN 'sold'
    ELSE 'ended'
  END
  WHERE status = 'active'
  AND ends_at <= NOW();
  $$
);

امتداد مكافحة القنص

تقوم معظم منصات المزاد بتمديد الموعد النهائي إذا جاء عرض خلال آخر بضع ثوان:

-- أضفها إلى مشغل process_new_bid
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
  UPDATE auctions
  SET ends_at = ends_at + INTERVAL '30 seconds'
  WHERE id = NEW.auction_id;
END IF;

Supabase Realtime مقابل البدائل

لقد استخدمت معظم هذه في الإنتاج. إليك مقارنة صادقة:

الميزة Supabase Realtime Pusher Ably Firebase RTDB Socket.io (مستضاف ذاتياً)
مزامنة قاعدة البيانات الأصلية ✅ PostgreSQL WAL ❌ خدمة منفصلة ❌ خدمة منفصلة ✅ شجرة JSON ❌ يدوي
زمن الكمون (p99) ~80-100 ميلي ثانية ~60 ميلي ثانية ~50 ميلي ثانية ~100 ميلي ثانية ~40 ميلي ثانية (يعتمد على البنية التحتية)
أقصى أحداث/ثانية 200 كيلو+ 10 كيلو (Pro) 50 كيلو 100 كيلو غير محدود (تقوم بتوسيع نطاقها)
دمج المصادقة مدمج (RLS + JWT) مخصص قائم على الرمز Firebase Auth مخصص
الحضور ✅ مدمج ✅ مدمج ✅ مدمج ✅ مدمج ✅ مدمج
الطبقة المجانية 500K MAU, 200 متزامن 100 اتصال 6M رسالة/شهر 1GB مخزن $0 (تكاليف الاستضافة)
تسعير Pro $25/شهر $49/شهر $29/شهر الدفع حسب الاستخدام ~$100-500/شهر (AWS)
الأفضل لـ تطبيقات الوقت الفعلي المركزة على قاعدة البيانات pub/sub بسيط موثوقية عالية تطبيقات الجوال التحكم الكامل

بالنسبة لنظام المزاد على وجه التحديد، يفوز Supabase لأن العروض الخاصة بك موجودة بالفعل في PostgreSQL. لا تحتاج إلى المزامنة بين قاعدة بيانات وسيستم pub/sub منفصل. يصل العرض إلى قاعدة البيانات، تشغل قاعدة البيانات دفع WebSocket. مصدر حقيقة واحد.

إذا كنت تبني على معمارية CMS بدون رأس، يتناسب Supabase بشكل طبيعي إلى جانب توصيل المحتوى بدون إضافة خدمة أخرى للإدارة.

نشر وتوسيع نطاق نظام المزاد الخاص بك

بالنسبة لمعظم المشاريع، تتعامل طبقة Supabase Pro المدارة في $25/شهر بسهولة مع 10,000 مزاد يومي. إليك ما يجب الانتباه له:

  • حدود الاتصال: تعطيك طبقة Pro 500 اتصال Realtime متزامن. إذا كنت بحاجة إلى المزيد، ستحتاج إلى الترقية أو تنفيذ تجميع الاتصالات على العميل.
  • حجم WAL: المزايدة عالية الحجم تولد حركة WAL كبيرة. راقب فتحة النسخ المتماثلة لتجنب انتفاخ القرص.
  • عدد القنوات: كل مزاد يحصل على قناته الخاصة. مع آلاف المزادات النشطة، اختبر أن العميل يلغي الاشتراك بشكل صحيح من المزادات المنتهية.

بالنسبة لواجهة أمامية مبنية باستخدام Astro أو Next.js، يعمل عميل Supabase JS بشكل متطابق — فقط تأكد من أنك تقوم بتهيئته من جانب العميل لاشتراكات Realtime.

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

الأسئلة الشائعة

كم عدد المزايدين المتزامنين الذي يمكن لـ Supabase Realtime التعامل معهم؟ يمكن لـ Supabase Realtime التعامل مع أكثر من 200,000 حدث في الثانية عبر خوادم موزعة على منصتهم المدارة. تدعم طبقة Pro في $25/شهر ما يصل إلى 500 اتصال متزامن لكل مشروع. بالنسبة للمزادات الأكبر، تقدم طبقة Enterprise حدوداً مخصصة، أو يمكنك استضافة خادم Realtime بنفسك (وهو مفتوح المصدر) على البنية التحتية الخاصة بك.

هل Supabase Realtime سريع كافي لمزاد مباشر؟ نعم. في الاختبارات الخاصة بي، يبلغ زمن الكمون من النهاية إلى النهاية من إدراج العرض إلى إخطار العميل حوالي 50-80 ميلي ثانية في المتوسط، مع p99 أقل من 100 ميلي ثانية. للسياق، وقت رد فعل الإنسان حوالي 200-300 ميلي ثانية، لذا تظهر العروض فوراً فعلياً. الاختناق نادراً ما يكون Supabase — عادة ما يكون اتصال العميل بالشبكة.

كيف أمنع الشروط التنافسية عندما يقدم شخصان عروضاً في نفس الوقت؟ استخدم قفل صف مستوى PostgreSQL SELECT ... FOR UPDATE داخل دالة المشغل، أو استخدم أقفال استشارية عبر pg_advisory_xact_lock(). هذا يسلسل معالجة العرض لكل مزاد بحيث يتم التحقق من عرض واحد فقط في كل مرة. يحصل العرض "الخاسر" على التحقق من الصحة — إنه فقط يرى العرض الأعلى المحدث من الفائز ويما ينجح (إذا كان لا يزال أعلى) أو يفشل برسالة خطأ مناسبة.

هل يمكنني استخدام Supabase Realtime مع Next.js أو Astro؟ بالتأكيد. يعمل عميل @supabase/supabase-js في أي بيئة JavaScript. بالنسبة لـ Next.js، قم بتهيئة عميل Supabase في مكون عميل (لأن Realtime يحتاج إلى WebSockets في المتصفح) واستخدمه داخل خطافات useEffect. بالنسبة لـ Astro، استخدمه في جزر تفاعلية من جانب العميل. رمز الاشتراك متطابق بغض النظر عن اختيار الإطار الخاص بك.

ماذا يحدث إذا انقطع اتصال المستخدم أثناء المزاد؟ يحاول Supabase Realtime إعادة الاتصال تلقائياً. عندما يعيد الاتصال والعميل الاشتراك، يتلقى الحالة الحالية. بالنسبة للمزادات الحرجة، أوصي أيضاً بجلب أحدث حالة مزاد عبر استعلام قياسي عند إعادة الاتصال للتأكد من عدم فقدان أي شيء أثناء نافذة قطع الاتصال. سيزيل نظام الحضور تلقائياً المستخدم المقطوع بعد انتهاء المهلة الزمنية.

كيف أتعامل مع أوقات انتهاء المزاد بدقة؟ لا تثق بمؤقتات جانب العميل — يمكن التلاعب بها. استخدم امتداد PostgreSQL pg_cron للتحقق من وإغلاق المزادات المنتهية كل 10 ثوان من جانب الخادم. أرسل طابع زمني خادم للعملاء حتى يتمكنوا من عرض عد تنازلي، لكن تحديد الانتهاء الفعلي يحدث دائماً في قاعدة البيانات.

هل Supabase Realtime مجاني للمشاريع الصغيرة؟ تتضمن الطبقة المجانية من Supabase Realtime مع ما يصل إلى 200 اتصال متزامن و 500,000 مستخدم نشط شهري. هذا كافي لموقع مزاد هواية أو MVP. إذا كنت تدير منصة مزاد إنتاجية بحركة مرور ذات مغزى، فإن طبقة Pro في $25/شهر مع $0.09/GB نقل بيانات هو المكان الذي ستريد البدء منه. إنه أرخص بكثير من تشغيل البنية التحتية الخاصة بك للـ WebSocket.

كيف أختبر نظام مزايدة حي محلياً؟ استخدم CLI Supabase (supabase start) لتشغيل مثيل Supabase محلي مع تفعيل Realtime. افتح علامات تبويب متعددة في المتصفح لمحاكاة مزايدين متعددين. للاختبار تحت الحمل، أستخدم نص Node.js بسيط يقوم بإنشاء 100+ عملاء Supabase ويجعلهم يتنافسون على بعضهم البعض على مزاد على مؤقت. هذا يلتقط الشروط التنافسية ويساعدك على ضبط معامل eventsPerSecond قبل الانتقال إلى الإنتاج.