Postgres LISTEN/NOTIFY: شرح التواجد في الوقت الفعلي
٥ مايو ٢٠٢٦
ملخص
يستعرض هذا البرنامج التعليمي بناء نظام تواجد "من متصل الآن" في الوقت الفعلي على PostgreSQL 18.31 باستخدام LISTEN / NOTIFY وبرنامج التشغيل pg 8.20.02، مع طبقة توزيع بسيطة باستخدام Express 5.2.13 + ws 8.20.04 تقوم بدفع تغييرات التواجد إلى المتصفحات عبر WebSockets. لا حاجة لـ Redis، ولا وسيط رسائل، ولا خدمة خارجية للوقت الفعلي — فقط SQL وحوالي 120 سطرًا من Node.js يمكنك نسخها ولصقها وتشغيلها في حوالي عشرين دقيقة على Docker Desktop أو أي مضيف Linux.
ما ستتعلمه
تحول قناة LISTEN / NOTIFY قاعدة بيانات Postgres نفسها إلى ناقل pub/sub: أي جلسة تصدر NOTIFY my_channel, 'payload' (أو تستدعي pg_notify('my_channel','payload')) توقظ كل جلسة أخرى قامت بتشغيل LISTEN my_channel، مع تسليم الحمولة (payloads) للمستمعين فقط بعد إتمام عملية النشر (commit)5. (الإخطارات المكررة — نفس القناة، نفس الحمولة، نفس المعاملة — قد يتم دمجها في تسليم واحد، لذا صمم الحمولات بحيث تكون التكرارات داخل معاملة واحدة مقصودة.) في هذا البرنامج التعليمي، ستقوم بتشغيل Postgres 18 في Docker، وتصميم جدول presence بسيط مع مشغل (trigger) على مستوى الصف ينشر حمولة JSON عبر pg_notify كلما تغيرت حالة المستخدم، وكتابة مستمع Node.js آمن عند إعادة الاتصال مبني على عميل pg مخصص (ليس pool — لأن الـ pools تقوم بتدوير الاتصالات، مما يكسر LISTEN بصمت)، وربط تلك الإخطارات بعملاء المتصفح عبر ws، وبناء صفحة اختبار HTML صغيرة تعرض تغييرات الحالة المباشرة، والتحقق من العملية بالكامل من البداية للنهاية باستخدام واجهتي psql. ستتعلم أيضًا الحدود القصوى لـ LISTEN / NOTIFY — حد الـ 8000 بايت للحمولة، وسقف 8 جيجابايت لزمام الإخطارات، وتسلسل الزمام العالمي الواحد الذي يجعل معدلات الإخطار العالية جدًا نقطة نزاع — حتى تعرف متى يكون هذا النمط هو الأداة المناسبة ومتى يجب عليك اللجوء إلى Redis أو NATS بدلاً من ذلك.
المتطلبات الأساسية
قبل البدء، تأكد من أن بيئتك المحلية تطابق هذه الإصدارات. ستعمل إصدارات Node و Postgres الأقدم على نفس الكود، لكن بعض واجهات البرمجة (APIs) وعلامات Docker أدناه تفترض وجود برامج حديثة.
- محرك Docker إصدار 23.0+ (Docker Desktop 4.19+ على macOS أو Windows، والذي يتضمن Moby 23.06) ليعمل إصدار
postgres:18-alpineالرسمي بسلاسة. تعمل محركات Docker الأقدم أيضًا، لكن علامات18-alpineو18.3-alpine3.23تفترض وجود عميل حديث. - Node.js 24.15.0 (إصدار LTS النشط اعتبارًا من أبريل 2026) أو Node.js 22.x (إصدار LTS للصيانة حتى أبريل 2027)7. يتطلب Express 5 إصدار Node 18 أو أحدث3، ويدعم
pg8.20.0 أي إصدار لا يزال في مرحلة LTS. - واجهة POSIX shell. تفترض الأمثلة أدناه استخدام
bashأوzshعلى macOS أو Linux. على Windows، قم بتشغيلها داخل WSL2. curlوpsql(الأخير يأتي مع حزمةpostgresql-clientعلى Debian/Ubuntu، أو حزمةpostgresqlعلى RHEL/Fedora، أو عبرbrew install libpqعلى macOS).- حوالي 200 ميجابايت من مساحة القرص الحرة لصورة Postgres ووحدات Node.
نحن نعتمد على PostgreSQL 18.3، وهو أحدث تصحيح في سلسلة 18 وقت كتابة هذا التقرير (تم إصداره في 26 فبراير 2026 كجزء من التحديث المشترك 18.3 / 17.9 / 16.13 / 15.17 / 14.221). أضاف Postgres 18.0 الإدخال/الإخراج غير المتزامن للمسح المتسلسل وعمليات vacuum8؛ تعمل ميزة LISTEN / NOTIFY بنفس الطريقة التي كانت تعمل بها منذ Postgres 9.0، لذا فإن الكود في هذا البرنامج التعليمي يعمل دون تغيير على Postgres 14 أو أحدث إذا لم تتمكن من الترقية بعد.
الخطوة 1: تشغيل Postgres 18
أنشئ دليل مشروع جديد وملف Docker-compose.yml يحدد إصدار التصحيح بالضبط. تحديد postgres:18.3-alpine3.23 (بدلاً من postgres:18-alpine أو postgres:latest) يمنحك بناءً قابلاً لإعادة الإنتاج عبر الأجهزة المختلفة.
mkdir presence-demo && cd presence-demo
cat > Docker-compose.yml <<'YAML'
services:
db:
image: postgres:18.3-alpine3.23
environment:
POSTGRES_PASSWORD: presence_dev_pw
POSTGRES_DB: presence
ports:
- "5432:5432"
volumes:
- presence-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d presence"]
interval: 2s
timeout: 3s
retries: 10
volumes:
presence-data:
YAML
Docker compose up -d
انتظر حتى تصبح الحاوية جاهزة (يجب أن يظهر (healthy) بجانب db عند تشغيل Docker compose ps)، ثم تأكد من أن الخادم يعمل بالإصدار 18.3:
Docker compose exec db psql -U postgres -d presence -c "SELECT version();"
المخرجات المتوقعة (مختصرة):
PostgreSQL 18.3 on x86_64-pc-linux-musl, compiled by gcc ...
إذا رأيت connection refused، فهذا يعني أن الحاوية لم تنتهِ من التهيئة بعد — فحص الحالة pg_isready أعلاه هو ما تستخدمه معظم أنظمة CI لربط الخدمات التابعة، ويمكنك محاكاته باستخدام until Docker compose exec db pg_isready -U postgres; do sleep 1; done.
الخطوة 2: تهيئة مشروع Node وتثبيت التبعيات المحددة
npm init -y
npm install pg@8.20.0 express@5.2.1 ws@8.20.0
npm install --save-dev nodemon@3.1.14
افتح package.json وأضف نص start بالإضافة إلى علامة type: module حتى نتمكن من استخدام صيغة import العادية بدون TypeScript:
{
"name": "presence-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon --watch . --ext js,html server.js"
},
"dependencies": {
"express": "5.2.1",
"pg": "8.20.0",
"ws": "8.20.0"
},
"devDependencies": {
"nodemon": "3.1.14"
}
}
احفظ ملف .env (أو قم بتصديره مباشرة) بحيث يعيش نص الاتصال خارج الكود:
export DATABASE_URL='postgres://postgres:presence_dev_pw@localhost:5432/presence'
الخطوة 3: إنشاء مخطط التواجد (schema)
أنشئ ملف schema.sql بجدول presence صغير، ومشغل يرسل JSON، وقناة إخطار واحدة تسمى presence_change:
-- schema.sql
CREATE TABLE IF NOT EXISTS presence (
user_id text PRIMARY KEY,
status text NOT NULL CHECK (status IN ('online', 'away', 'offline')),
last_seen timestamptz NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION notify_presence_change()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
payload jsonb;
BEGIN
payload := jsonb_build_object(
'user_id', NEW.user_id,
'status', NEW.status,
'last_seen', NEW.last_seen
);
-- pg_notify is the function form; payload must be < 8000 bytes total
-- including the channel name. Keep payloads small or store a row id and
-- have listeners SELECT the full record.
PERFORM pg_notify('presence_change', payload::text);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS presence_change_trigger ON presence;
CREATE TRIGGER presence_change_trigger
AFTER INSERT OR UPDATE OF status, last_seen ON presence
FOR EACH ROW
EXECUTE FUNCTION notify_presence_change();
قم بتطبيق المخطط:
Docker compose exec -T db psql -U postgres -d presence < schema.sql
المخرجات المتوقعة (يظهر NOTICE فقط في التشغيل الأول، عندما لا يكون المشغل موجودًا بعد):
CREATE TABLE
CREATE FUNCTION
NOTICE: trigger "presence_change_trigger" for relation "presence" does not exist, skipping
DROP TRIGGER
CREATE TRIGGER
حد الـ 8000 بايت للحمولة هو سلوك موثق لـ LISTEN / NOTIFY: في التكوين الافتراضي، يجب أن يكون الوسيط الثاني لـ pg_notify بالإضافة إلى اسم القناة أصغر من 8000 بايت إجمالاً5. بالنسبة لبيانات التواجد، هذا جيد، ولكن إذا كنت تريد دفع نص رسالة دردشة كاملة مثلاً، فإن النمط الموصى به هو كتابة الصف في جدول و NOTIFY بمعرف الصف فقط — ثم يقوم المستمعون بـ SELECT للنص بشكل منفصل.
الخطوة 4: بناء مستمع آمن عند إعادة الاتصال
أنشئ ملف listener.js. مساحة الـ API الأساسية من pg صغيرة: افتح Client مخصصًا (وليس Pool، لأن الـ pools تعيد تدوير الاتصالات وفي اللحظة التي يتم فيها تدوير اتصال LISTEN، ستتوقف عن تلقي الإخطارات بصمت)، وأصدر أمر LISTEN، ثم اربط معالجًا بحدث 'notification' الخاص بالعميل9.
// listener.js
import pg from 'pg';
const { Client } = pg;
// Postgres unquoted identifiers must start with a letter or underscore and
// contain only letters, digits, or underscores. We validate the channel name
// up front so we can safely interpolate it into the LISTEN command — channel
// names are SQL identifiers, not values, so they cannot be parameterized
// through $1.
const SAFE_CHANNEL = /^[a-z_][a-z0-9_]{0,62}$/;
export function createListener({ connectionString, channel, onNotify }) {
if (!SAFE_CHANNEL.test(channel)) {
throw new Error(`unsafe channel name: ${channel`);
}
let client;
let stopped = false;
let backoffMs = 500;
async function connect() {
if (stopped) return;
client = new Client({ connectionString );
client.on('error', handleError);
client.on('notification', (msg) => {
// msg = { processId, channel, payload }
if (!msg.payload) return;
try {
onNotify(JSON.parse(msg.payload));
} catch (err) {
console.error('presence: bad payload', msg.payload, err);
}
});
try {
await client.connect();
await client.query(`LISTEN ${channel`);
console.log(`presence: listening on "${channel"`);
backoffMs = 500; // reset after a healthy connect
} catch (err) {
handleError(err);
}
}
function handleError(err) {
console.error('presence: client error, reconnecting', err.code || err.message);
if (client) {
client.removeAllListeners();
client.end().catch(() => {});
client = null;
}
if (stopped) return;
setTimeout(connect, backoffMs);
backoffMs = Math.min(backoffMs * 2, 15_000);
}
function stop() {
stopped = true;
if (client) client.end().catch(() => {});
}
connect();
return { stop };
}
ثلاثة أشياء تستحق الملاحظة في هذا المستمع:
- تعبير
SAFE_CHANNELالنمطي (regex) — أسماء قنوات Postgres هي معرفات SQL وليست قيمًا، لذا لا يمكن تمريرها كبارامترات عبر$1. أفضل وسيلة دفاع هي تقييد أسماء القنوات بأبجدية المعرفات غير المقتبسة مسبقًا ورفض أي شيء آخر؛ يحدد التعبير النمطي الطول الإجمالي بـ 63 حرفًا (حرف أول أو شرطة سفلية بالإضافة إلى ما يصل إلى 62 حرفًا تاليًا)، وهو ما يطابق حد المعرفات المشتق منNAMEDATALENفي Postgres والبالغ 63 بايت10 — أي شيء أطول سيتم قطعه بصمت بواسطة الخادم. - التراجع الأسي (Exponential backoff) بحد أقصى 15 ثانية — عندما تعيد قاعدة البيانات التشغيل أثناء عملية النشر، سيحاول كل مستمع إعادة الاتصال في وقت واحد. يمنع هذا الحد الأقصى حدوث "القطيع المتدافع" (thundering herd)، كما أن إعادة تعيين
backoffMsبعد اتصال سليم يمنع التقييد الدائم بعد ليلة واحدة سيئة. - إعادة إصدار
LISTENعند كل إعادة اتصال — يتم تسجيلLISTENمقابل جلسة الخلفية الحالية، لذا فإن الاتصال المقطوع ينسى جميع الاشتراكات؛ يجب عليك إعادة تنفيذLISTENبعد كل إعادة اتصال، وإلا ستذهب التنبيهات بصمت إلى سلة المهملات الرقمية. هذا هو الخطأ الأكثر شيوعًا في إعداداتLISTEN/NOTIFYفي بيئات الإنتاج11.
الخطوة 5: الجسر إلى المتصفحات باستخدام Express + ws
قم بإنشاء ملف server.js الذي يربط المستمع بخادم WebSocket:
// server.js
import express from 'express';
import { WebSocketServer } from 'ws';
import { createServer } from 'node:http';
import { createListener } from './listener.js';
const app = express();
app.use(express.static('public'));
app.use(express.json());
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
wss.on('connection', (ws) => {
ws.on('error', (err) => console.error('ws error', err));
});
function broadcast(event) {
const data = JSON.stringify(event);
for (const client of wss.clients) {
if (client.readyState === 1 /* OPEN */) client.send(data);
}
}
createListener({
connectionString: process.env.DATABASE_URL,
channel: 'presence_change',
onNotify: (event) => broadcast(event),
});
const PORT = Number(process.env.PORT || 3000);
httpServer.listen(PORT, () => {
console.log(`presence: http on http://localhost:${PORT`);
});
يعتبر Express 5 (الوسم الافتراضي latest على npm منذ مارس 202512) غلافًا رقيقًا هنا — الشيء الوحيد الذي يفعله هو تقديم مجلد public/ ثابت. العمل الحقيقي يحدث في ws: يتم توزيع كل تنبيه من Postgres على كل متصفح متصل. بالنسبة لنشر إنتاجي جاد، ستقوم بحصر التنبيهات للمستخدمين المصادق عليهم؛ أما في هذا العرض التجريبي، يرى كل متصفح كل تغيير في الحالة.
الخطوة 6: عميل المتصفح
قم بإنشاء ملف public/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Presence demo</title>
<style>
body { font-family: -apple-system, sans-serif; margin: 2rem; }
li { padding: 0.25rem 0; }
.online { color: #15803d; }
.away { color: #ca8a04; }
.offline { color: #6b7280; text-decoration: line-through; }
</style>
</head>
<body>
<h1>Live presence</h1>
<p>Open two terminals and run the SQL in step 8 — the list below updates in real time.</p>
<ul id="users"></ul>
<script>
const users = new Map();
const list = document.getElementById('users');
function render() {
list.innerHTML = '';
for (const [id, { status, last_seen ] of users) {
const li = document.createElement('li');
li.className = status;
li.textContent = `${id — ${status (last seen ${last_seen)`;
list.appendChild(li);
const ws = new WebSocket(`ws://${location.host/ws`);
ws.addEventListener('message', (event) => {
const e = JSON.parse(event.data);
users.set(e.user_id, { status: e.status, last_seen: e.last_seen );
render();
);
ws.addEventListener('close', () => {
// Trivial reconnect: reload after 1s. In production, do exponential backoff
// on the client too, and resync state from a REST endpoint after reconnect.
setTimeout(() => location.reload(), 1000);
);
</script>
</body>
</html>
هذا الكود بسيط عمدًا — بدون إطار عمل أو أداة تجميع. الهدف هو إظهار المسار الكامل من البداية للنهاية بأقل عدد ممكن من الأجزاء المتحركة.
الخطوة 7: التشغيل
في نافذة طرفية واحدة:
npm start
المخرجات المتوقعة:
presence: http on http://localhost:3000
presence: listening on "presence_change"
افتح http://localhost:3000 في متصفحك. ستكون القائمة فارغة حتى تقوم بكتابة صف في الخطوة التالية.
الخطوة 8: التحقق من المسار الكامل
افتح نافذة طرفية ثانية وقم بتشغيل بعض تحديثات التواجد مقابل قاعدة البيانات:
Docker compose exec db psql -U postgres -d presence
ثم في صدفة psql:
INSERT INTO presence (user_id, statusVALUES ('alice', 'online';
INSERT INTO presence (user_id, statusVALUES ('bob', 'online';
UPDATE presence SET status = 'away' WHERE user_id = 'alice';
UPDATE presence SET status = 'offline' WHERE user_id = 'bob';
يجب أن تظهر كل جملة في المتصفح فورًا. لن تطبع وحدة تحكم server.js أي شيء جديد (نحن نسجل فقط عند الاتصال / الخطأ)، ولكن يمكنك التحقق من أن القناة نشطة مباشرة في psql عن طريق الاستماع بنفسك:
LISTEN presence_change;
NOTIFY presence_change, '{"user_id":"carol","status":"online","last_seen":"now"}';
سيقوم psql بطباعة:
Asynchronous notification "presence_change" with payload "{...}" received from server process with PID NNNN.
لاختبار الحمل، سيقوم نص مخصص صغير لـ pgbench بإنشاء تنبيهات بمعدلات مستدامة، ولكن كن على دراية بحد طابور التنبيهات البالغ 8 جيجابايت — يتم تخزين الطابور على القرص تحت مجلد pg_notify/ الخاص بالمجموعة مع تعيين الصفحات النشطة في الذاكرة المشتركة عبر نظام SLRU الفرعي، وبمجرد امتلائه، ستفشل المعاملات التي تصدر NOTIFY عند التأكيد (commit) حتى يقوم المستمعون بتفريغ المتراكم. يمكنك مراقبة صحة الطابور باستخدام:
SELECT pg_notification_queue_usage(; -- 0.0 = فارغ, 1.0 = ممتلئ
ترجع الدالة نسبة طابور التنبيهات المشغولة حاليًا5. أي شيء أعلى باستمرار من ~0.5 هو علامة على وجود مستمع بطيء أو ميت.
الخطوة 9: متى يكون LISTEN/NOTIFY هو الأداة المناسبة — ومتى لا يكون كذلك
تعتبر LISTEN / NOTIFY ممتازة لتوزيع الأحداث بمعدل منخفض إلى متوسط حيث تريد نفس ضمانات المعاملات مثل بقية بياناتك: مؤشرات التواجد، إبطال ذاكرة التخزين المؤقت، تنبيهات "تعليق جديد"، لوحات تحكم المسؤولين، إشارات انتهاء الوظائف البطيئة. لكنها تخسر أمام وسيط مخصص مثل Redis Pub/Sub أو NATS في ثلاث نقاط ملموسة:
- تأخذ
NOTIFYقفلًا حصريًا عالميًا أثناء التأكيد (commit). يرجع تحقيق Recall.ai في حالات انقطاع الخدمة الناتجة عن LISTEN/NOTIFY هذا إلى "mutex" عالمي واحد يتم الاستحواذ عليه بواسطة نظام التنبيهات الفرعي أثناء التأكيد، مما يؤدي فعليًا إلى تسلسل عمليات التأكيد عبر المجموعة بأكملها عندما يقوم العديد من الكتاب المتزامنين بإصدارNOTIFY13. بالنسبة لمعدلات التنبيه المنخفضة إلى المتوسطة، يكون هذا غير مرئي؛ وتحت ضغط الكتابة المتزامنة الثقيلة، يصبح هو عنق الزجاجة. - لا توجد مجموعات مستهلكين / إعادة تشغيل (replay). المستمع الذي ينقطع اتصاله يفقد كل ما حدث أثناء غيابه. إذا كانت المتانة (durability) تهمك، فاكتب في جدول وقم بإصدار
NOTIFYلمعرف الصف فقط، واجعل المستمعين يتصالحون مع البيانات باستخدامSELECTبعد إعادة الاتصال. هذا هو نفس النمط الذي استخدمته PgQ و Skytools لعقدين من الزمن. - حد حمولة 8000 بايت هو حد صارم. لا يمكن تهيئته في وقت التشغيل؛ فهو محدد بواسطة
NOTIFY_PAYLOAD_MAX_LENGTHفي كود المصدر للخادم. إذا كان من الممكن أن تكون رسائلك أكبر من ذلك، فصمم نظامك حول هذا الحد من اليوم الأول.
بالنسبة لغرف الدردشة التي تضم مئات الآلاف من المشتركين والتحجيم لكل قناة، استعن بوسيط مخصص. أما بالنسبة للتواجد، وسجلات المراجعة، وأحداث انتهاء الوظائف البطيئة، ومعظم حالات الاستخدام من نوع "أريد فقط تحديث لوحة التحكم الخاصة بي"، فإن LISTEN / NOTIFY تحافظ على بساطة بنيتك التقنية وتمنحك اتساق المعاملات مجانًا.
الأخطاء الشائعة
- "المستمع الخاص بي يتوقف عن تلقي الأحداث بعد عملية النشر." من المؤكد تقريبًا أنك تستخدم
Poolبدلاً منClientمخصص. تستحوذ المجمعات (Pools) على اتصال جديد لكل استعلام، ويتم تحرير الاتصال الذي أصدرLISTENبمجرد عودة الاستعلام، لذا تذهب التنبيهات إلى اتصال لا يقرأ منه شيء. استخدمnew Client(...)حصريًا للمستمع9. - "التنبيهات تتجمع في نهاية معاملة بطيئة." هذا هو السلوك الموثق — يتم تسليم الأحداث من المعاملة فقط عند
COMMIT، وليس أبدًا في منتصف المعاملة5. إذا كنت بحاجة إلى إشارات تقدم أثناء معاملة طويلة الأمد، فقم بتشغيل العمل خارج معاملة أو في وظائف خلفية بنمط المعاملات المستقلة. - "بعض الحمولات تختفي بصمت." تحقق من
pg_notification_queue_usage()— عندما تقترب من 1.0 يكون الطابور ممتلئًا وسيرفض Postgres تأكيد المزيد من المعاملات التي تصدرNOTIFYحتى يلحق المستهلكون بالركب. الحل هو مستهلكون أسرع، وليس طابورًا أكبر. - "أواجه أخطاء
payload string too long." اسم القناة بالإضافة إلى الحمولة يتجاوز 8000 بايت. انقل محتوى الرسالة إلى صف في جدول وقم بالتنبيه باستخدام المفتاح الأساسي فقط. - "حمولة JSON الخاصة بي تظهر مقطوعة عندما تحتوي على رموز تعبيرية (emoji)." يحسب Postgres حد الـ 8000 بايت بالبايت وليس بالأحرف. رموز UTF-8 التعبيرية تبلغ 4 بايت لكل منها، لذا فإن حمولة مكونة من 2000 رمز تعبيري خالص تبلغ 8000 بايت. إما أن تقيد طول الحمولة بـ ~1500 حرف لتكون آمنًا، أو اتبع نمط معرف الصف المذكور أعلاه.
الخطوات التالية ومزيد من القراءة
إذا استمتعت بهذا النمط، فإن دليل بنيات قواعد البيانات القوية على نيرد ليفل تك يغطي مساحة المقايضة الأوسع بين pub/sub داخل قاعدة البيانات، ووسطاء الرسائل (message brokers)، ومسارات CDC، بينما يستعرض دليل أنماط التخزين المؤقت لـ Redis البديل الأقرب عندما لا تعود أدوات LISTEN / NOTIFY هي الأداة المناسبة. منطق إعادة الاتصال من الخطوة 4 هو نفس الشكل الذي ستكتبه لأي اتصال قاعدة بيانات طويل الأمد. بالنسبة لـ Postgres 18 نفسها، فإن إعلان إصدار PG 18 الرسمي هو أفضل ملخص لنظام الإدخال/الإخراج غير المتزامن الفرعي وعرض النظام الخاص به pg_aios14.
الخطأ الأكثر شيوعاً في بيئات الإنتاج مع LISTEN / NOTIFY هو نسيان إعادة تنفيذ LISTEN بعد إعادة الاتصال؛ والثاني هو استخدام Pool بدلاً من Client مخصص. إذا كان الكود الخاص بك في الخطوة 4 يشبه الإصدار أعلاه، فقد تجنبت بالفعل كليهما.
Footnotes
-
PostgreSQL 18.3, 17.9, 16.13, 15.17, and 14.22 release announcement, February 26, 2026. https://www.postgresql.org/about/news/postgresql-183-179-1613-1517-and-1422-released-3246/ ↩ ↩2
-
pg8.20.0 on npm. https://www.npmjs.com/package/pg ↩ -
Express 5.2.1 on npm. https://www.npmjs.com/package/express. Express 5 announcement and Node 18+ requirement: https://expressjs.com/2025/03/31/v5-1-latest-release.html ↩ ↩2
-
ws8.20.0 on npm. https://www.npmjs.com/package/ws ↩ -
PostgreSQL 18 documentation, NOTIFY: payload size limit (8000 bytes including channel name), 8 GB queue,
pg_notification_queue_usage(), transactional delivery semantics. https://www.postgresql.org/docs/current/sql-notify.html ↩ ↩2 ↩3 ↩4 -
Docker Desktop 4.19 release notes (Moby 23.0 engine update). https://www.Docker.com/blog/Docker-desktop-4-19/ ↩
-
Node.js release schedule (Node 24 active LTS since October 2025; Node 22 maintenance LTS through April 2027). https://nodejs.org/en/about/previous-releases ↩
-
PostgreSQL 18 release announcement (September 25, 2025): asynchronous I/O subsystem,
io_method,pg_aiosview. https://www.postgresql.org/about/news/postgresql-18-released-3142/ ↩ -
node-postgres Client API (events:
notification,error,end). https://node-postgres.com/apis/client ↩ ↩2 -
PostgreSQL 18 lexical structure:
NAMEDATALENdefaults to 64, so identifiers are at most 63 bytes; longer names are silently truncated. https://www.postgresql.org/docs/current/sql-syntax-lexical.html ↩ -
andywer/pg-listen README, "Why?" section — documents the reconnect-and-re-LISTEN failure mode that motivated the library. https://GitHub.com/andywer/pg-listen ↩
-
Express 5.1.0 release post (Express 5 made the default
lateston npm in March 2025). https://expressjs.com/2025/03/31/v5-1-latest-release.html ↩ -
Recall.ai engineering blog, "Postgres LISTEN/NOTIFY does not scale" — describes the single global queue and contention behavior under heavy notify traffic. https://www.recall.ai/blog/postgres-listen-notify-does-not-scale ↩
-
PostgreSQL 18 asynchronous I/O feature matrix entry. https://www.postgresql.org/about/featurematrix/detail/asynchronous-io-aio/ ↩