Messenger/packages/backend/src/modules/notifications/notifications.queue.ts
Заид Омар Медхат | Zaid Omar Medhat 47fbf861ee Phase 6.5: notifications service + in-app toasts; in-app voice/video messages; SVG icons
Notifications (web push now, Android FCM ready, iOS prepared):
- device_tokens / notification_settings / conversation_mutes (migration 006)
- REST: register/list/unregister devices, get/update settings (quiet hours,
  timezone, enable), mute/unmute conversation, VAPID public key
- BullMQ queue: backend enqueues a job per new message; a dedicated stateless
  `notifications` worker service drains it
- Delivery gating: skip sender, muted chats, disabled users, quiet hours, and
  ONLINE users (they get the message live + an in-app toast) — offline → push
- Providers behind one interface: Web Push (VAPID/web-push) + FCM (firebase-admin,
  HTTP v1; Android + iOS via the same path). Dead tokens (404/410/unregistered)
  auto-retired; transient failures recorded
- Web: service worker (push display + tap-to-open the originating chat), push
  registration/unregistration, in-app toast stack, deep-link via ?conversation=

In-app voice & video messages: MediaRecorder capture in the composer (mic/video),
live preview + timer, upload via the existing presigned-media path.

No emojis anywhere: replaced all glyphs with inline SVG icons (shared/ui) so the
UI renders identically across web/desktop/mobile; notification text is plain.

Chores: dedupe ioredis (BullMQ) + pin uuid>=11.1.1 (audit clean) via pnpm overrides;
reusable Centrifugo client factory for the worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
2026-07-10 19:05:39 +05:00

45 lines
1.2 KiB
TypeScript

import { Queue } from 'bullmq';
import { Redis } from 'ioredis';
// A new message that may warrant a push. Only ids travel through the queue; the
// worker re-loads fresh state so nothing goes stale between enqueue and delivery.
export const MESSAGE_JOB = 'message';
export interface MessageJobData {
conversationId: string;
messageId: string;
senderId: string;
}
export interface NotificationQueue {
enqueueMessage(data: MessageJobData): Promise<void>;
close(): Promise<void>;
}
// BullMQ requires a dedicated connection with retry-per-request disabled.
export const createBullConnection = (redisUrl: string): Redis =>
new Redis(redisUrl, { maxRetriesPerRequest: null });
export const createNotificationQueue = (
queueName: string,
connection: Redis,
): NotificationQueue => {
const queue = new Queue<MessageJobData>(queueName, {
connection,
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: { count: 1000 },
removeOnFail: { count: 5000 },
},
});
return {
enqueueMessage: async (data) => {
await queue.add(MESSAGE_JOB, data);
},
close: async () => {
await queue.close();
},
};
};