Gerçek Zamanlı Bildirimler ve Çok Kanallı Teslimat: WebSocket, Push, Email ve Ötesi
WebSocket, push bildirim, email, SMS ve webhook kanalları için üretimde test edilmiş gerçek zamanlı bildirim teslimat stratejileri
Bir bildirim sistemi aynı kişiye birbirine hiç benzemeyen kanallardan ulaşmak zorunda: dakikalarla ölçülen bir socket, iki gönderim arasında geçersizleşebilen bir push token, çoktan suppression listesine düşmüş olabilecek bir e-posta adresi.
WebSocket’te connection drain, push’ta token geçersizleşmesi, email’de bounce yönetimi: her mekanizmanın kendi sınırları, hata modları ve gecikme beklentisi var. WebSocket saniyenin altında teslimat isterken email için birkaç dakika kabul edilebilir. Aşağıda her kanalın teslimat desenleri ve hangi kanalın ne zaman devreye gireceğine karar veren koordinasyon katmanı var.
WebSocket Bağlantı Yönetimi#
Bağlantı Durumunu Saklamak#
Aşağıdaki yönetici kullanıcı-socket eşlemesini Redis’te tutuyor; böylece bağlantıya sahip olmayan bir instance da onu bulabiliyor:
interface ConnectionMetadata {
userId: string;
deviceId?: string;
userAgent: string;
connectedAt: Date;
lastPing: Date;
subscriptions: Set<string>;
metadata: Record<string, any>;
}
class WebSocketConnectionManager {
private connections: Map<string, {
socket: WebSocket;
metadata: ConnectionMetadata;
}> = new Map();
private userConnections: Map<string, Set<string>> = new Map();
private redis: Redis;
private heartbeatInterval: NodeJS.Timeout;
constructor(redis: Redis) {
this.redis = redis;
this.startHeartbeat();
}
async handleConnection(socket: WebSocket, request: IncomingMessage): Promise<void> {
const connectionId = this.generateConnectionId();
try {
// JWT token veya session'dan kullanıcı bilgisi çıkar
const userInfo = await this.authenticateConnection(request);
if (!userInfo) {
socket.close(1008, 'Authentication required');
return;
}
const metadata: ConnectionMetadata = {
userId: userInfo.userId,
deviceId: userInfo.deviceId,
userAgent: request.headers['user-agent'] || '',
connectedAt: new Date(),
lastPing: new Date(),
subscriptions: new Set(),
metadata: {}
};
// Bağlantıyı sakla
this.connections.set(connectionId, { socket, metadata });
// Kullanıcı bağlantı mapping'ini güncelle
if (!this.userConnections.has(userInfo.userId)) {
this.userConnections.set(userInfo.userId, new Set());
}
this.userConnections.get(userInfo.userId)!.add(connectionId);
// Çok instance desteği için Redis'te bağlantı bilgisini sakla
await this.redis.hset(
`ws:connections:${userInfo.userId}`,
connectionId,
JSON.stringify({
serverId: process.env.SERVER_ID,
connectedAt: metadata.connectedAt,
deviceId: metadata.deviceId
})
);
// Event handler'ları kur
this.setupConnectionHandlers(connectionId, socket, metadata);
// Bağlantı onayı gönder
await this.sendMessage(connectionId, {
type: 'connection_ack',
data: { connectionId, timestamp: new Date() }
});
console.log(`WebSocket connection established for user ${userInfo.userId}`);
} catch (error) {
console.error('WebSocket connection setup failed:', error);
socket.close(1011, 'Internal server error');
}
}
private setupConnectionHandlers(
connectionId: string,
socket: WebSocket,
metadata: ConnectionMetadata
): void {
socket.on('message', async (data) => {
try {
const message = JSON.parse(data.toString());
await this.handleMessage(connectionId, message);
} catch (error) {
console.error('Message handling error:', error);
await this.sendError(connectionId, 'Invalid message format');
}
});
socket.on('pong', () => {
metadata.lastPing = new Date();
});
socket.on('close', async (code, reason) => {
await this.handleDisconnection(connectionId, code, reason);
});
socket.on('error', async (error) => {
console.error(`WebSocket error for ${connectionId}:`, error);
await this.handleDisconnection(connectionId, 1011, 'Connection error');
});
}
async sendNotificationToUser(userId: string, notification: NotificationEvent): Promise<void> {
const userConnectionIds = this.userConnections.get(userId) || new Set();
if (userConnectionIds.size === 0) {
// Kullanıcı bu sunucu instance'ına bağlı değil
// Diğer sunucu instance'ları için Redis'i kontrol et
const remoteConnections = await this.redis.hgetall(`ws:connections:${userId}`);
if (Object.keys(remoteConnections).length > 0) {
// Kullanıcı başka bir sunucu instance'ına bağlı
await this.redis.publish('ws:notification', JSON.stringify({
userId,
notification,
targetServerId: null // tüm sunuculara broadcast
}));
}
return;
}
// Yerel bağlantılara gönder
const sendPromises = Array.from(userConnectionIds).map(async (connectionId) => {
try {
await this.sendMessage(connectionId, {
type: 'notification',
data: notification
});
return { connectionId, success: true };
} catch (error) {
console.error(`Failed to send notification to ${connectionId}:`, error);
return { connectionId, success: false, error };
}
});
const results = await Promise.allSettled(sendPromises);
// Başarısız bağlantıları temizle
results.forEach((result, index) => {
if (result.status === 'rejected' ||
(result.status === 'fulfilled' && !result.value.success)) {
const connectionId = Array.from(userConnectionIds)[index];
this.handleDisconnection(connectionId, 1011, 'Send failed');
}
});
}
}
Instance’lar Arasında Koordinasyon#
Yatay ölçekleme, gönderenle bağlantının aynı süreçte olduğu varsayımını bozuyor. Redis pub/sub bu boşluğu kapatıyor:
class WebSocketCluster {
constructor(
private connectionManager: WebSocketConnectionManager,
private redis: Redis
) {
this.setupClusterCommunication();
}
private setupClusterCommunication(): void {
// Teslim edilmesi gereken bildirimler için dinle
this.redis.subscribe('ws:notification');
this.redis.subscribe('ws:broadcast');
this.redis.on('message', async (channel, message) => {
try {
const data = JSON.parse(message);
if (channel === 'ws:notification') {
await this.handleRemoteNotification(data);
} else if (channel === 'ws:broadcast') {
await this.handleBroadcast(data);
}
} catch (error) {
console.error('Cluster message handling error:', error);
}
});
}
private async handleRemoteNotification(data: {
userId: string;
notification: NotificationEvent;
targetServerId?: string;
}): Promise<void> {
// Sadece hedef sunucu belirtilmemiş veya biziz işle
if (data.targetServerId && data.targetServerId !== process.env.SERVER_ID) {
return;
}
await this.connectionManager.sendNotificationToUser(
data.userId,
data.notification
);
}
async broadcastSystemMessage(message: any): Promise<void> {
await this.redis.publish('ws:broadcast', JSON.stringify({
message,
senderId: process.env.SERVER_ID,
timestamp: new Date()
}));
}
}
Push Bildirim Teslimatı#
Çok Platformlu Push Servisi#
İkisi de “push bildirim” olsa da iOS ve Android’i tamamen farklı sistemler olarak ele almak gerekir:
interface PushProvider {
sendNotification(
tokens: string[],
payload: PushPayload,
options?: PushOptions
): Promise<PushResult[]>;
validateToken(token: string): Promise<boolean>;
getInvalidTokens(results: PushResult[]): string[];
}
interface PushPayload {
title: string;
body: string;
data?: Record<string, any>;
badge?: number;
sound?: string;
icon?: string;
image?: string;
}
class UnifiedPushService {
private providers: Map<PushPlatform, PushProvider> = new Map();
private tokenStore: TokenStore;
private analytics: PushAnalytics;
constructor() {
this.providers.set('ios', new APNSProvider());
this.providers.set('android', new FCMProvider());
this.providers.set('web', new WebPushProvider());
}
async sendPushNotification(
userId: string,
notification: NotificationEvent
): Promise<PushDeliveryResult> {
try {
// Kullanıcının tüm push token'larını al
const userTokens = await this.tokenStore.getUserTokens(userId);
if (userTokens.length === 0) {
return {
success: false,
reason: 'no_tokens',
deliveries: []
};
}
// Token'ları platformlara göre grupla
const tokensByPlatform = this.groupTokensByPlatform(userTokens);
// Platforma özel payload'ları hazırla
const payloads = await this.createPlatformPayloads(notification);
// Her platforme gönder
const deliveryPromises = Object.entries(tokensByPlatform).map(
([platform, tokens]) => this.sendToPlatform(
platform as PushPlatform,
tokens,
payloads[platform as PushPlatform],
notification
)
);
const results = await Promise.allSettled(deliveryPromises);
// Sonuçları işle ve geçersiz token'ları temizle
const deliveries = await this.processDeliveryResults(results, userTokens);
// Analitikleri takip et
await this.analytics.trackPushDelivery(notification.id, deliveries);
return {
success: deliveries.some(d => d.success),
deliveries
};
} catch (error) {
console.error('Push notification delivery failed:', error);
return {
success: false,
reason: 'send_error',
error: error.message,
deliveries: []
};
}
}
private async sendToPlatform(
platform: PushPlatform,
tokens: PushToken[],
payload: PushPayload,
notification: NotificationEvent
): Promise<PlatformDeliveryResult> {
const provider = this.providers.get(platform);
if (!provider) {
throw new Error(`No provider for platform ${platform}`);
}
// Platforma özel seçenekler
const options: PushOptions = {
priority: this.mapPriorityToPlatform(notification.priority, platform),
ttl: notification.expiresAt ?
Math.floor((notification.expiresAt.getTime() - Date.now()) / 1000) :
3600, // 1 saat default
collapseKey: platform === 'android' ? notification.type : undefined,
apnsTopic: platform === 'ios' ? process.env.APNS_TOPIC : undefined
};
const tokenStrings = tokens.map(t => t.token);
const results = await provider.sendNotification(tokenStrings, payload, options);
// Geçersiz token'ları temizle
const invalidTokens = provider.getInvalidTokens(results);
if (invalidTokens.length > 0) {
await this.tokenStore.markTokensInvalid(notification.userId, invalidTokens);
}
return {
platform,
tokens: tokenStrings,
results,
invalidTokens
};
}
private async createPlatformPayloads(
notification: NotificationEvent
): Promise<Record<PushPlatform, PushPayload>> {
// Kullanıcı tercihlerine göre lokalize içerik al
const template = await this.templateService.getTemplate(
notification.type,
'push',
'tr' // Kullanıcının locale'i olmalı
);
const rendered = await this.templateService.render(template, notification.data);
return {
ios: {
title: rendered.subject || '',
body: rendered.body,
data: {
notificationId: notification.id,
type: notification.type,
...notification.data
},
badge: await this.getBadgeCount(notification.userId),
sound: this.getSoundForNotificationType(notification.type)
},
android: {
title: rendered.subject || '',
body: rendered.body,
data: {
notificationId: notification.id,
type: notification.type,
...notification.data
},
icon: 'ic_notification',
// Android özel styling
color: '#007AFF'
},
web: {
title: rendered.subject || '',
body: rendered.body,
data: notification.data,
icon: '/icons/notification-icon.png',
image: notification.data.imageUrl
}
};
}
}
Push Token Yönetimi#
class PushTokenStore {
constructor(private db: Database, private redis: Redis) {}
async registerToken(
userId: string,
token: string,
platform: PushPlatform,
deviceId: string
): Promise<void> {
try {
// Token formatını doğrula
if (!this.isValidTokenFormat(token, platform)) {
throw new Error('Invalid token format');
}
// Token'ın başka bir kullanıcıda olup olmadığını kontrol et
const existingToken = await this.db.query(
'SELECT user_id FROM push_tokens WHERE token = $1',
[token]
);
if (existingToken.length > 0 && existingToken[0].user_id !== userId) {
// Token yeni kullanıcıya geçmiş, güncelle
await this.db.query(
'UPDATE push_tokens SET user_id = $1, updated_at = NOW() WHERE token = $2',
[userId, token]
);
} else {
// Token'ı ekle veya güncelle
await this.db.query(`
INSERT INTO push_tokens (user_id, token, platform, device_id, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, true, NOW(), NOW())
ON CONFLICT (token)
DO UPDATE SET
user_id = $1,
is_active = true,
updated_at = NOW()
`, [userId, token, platform, deviceId]);
}
// Hızlı lookup için aktif token'ları cache'le
await this.redis.sadd(`push_tokens:${userId}`, token);
console.log(`Push token registered for user ${userId} on ${platform}`);
} catch (error) {
console.error('Push token registration failed:', error);
throw error;
}
}
async markTokensInvalid(userId: string, tokens: string[]): Promise<void> {
if (tokens.length === 0) return;
await this.db.query(
'UPDATE push_tokens SET is_active = false, updated_at = NOW() WHERE token = ANY($1)',
[tokens]
);
// Redis cache'ten kaldır
if (tokens.length > 0) {
await this.redis.srem(`push_tokens:${userId}`, ...tokens);
}
console.log(`Marked ${tokens.length} tokens as invalid for user ${userId}`);
}
async getUserTokens(userId: string): Promise<PushToken[]> {
// Önce cache'i dene
const cachedTokens = await this.redis.smembers(`push_tokens:${userId}`);
if (cachedTokens.length > 0) {
// Veritabanından tam token bilgisini al
const tokens = await this.db.query(`
SELECT token, platform, device_id, created_at
FROM push_tokens
WHERE user_id = $1 AND is_active = true AND token = ANY($2)
`, [userId, cachedTokens]);
return tokens;
}
// Cache miss, veritabanından al ve cache'i doldur
const tokens = await this.db.query(`
SELECT token, platform, device_id, created_at
FROM push_tokens
WHERE user_id = $1 AND is_active = true
ORDER BY updated_at DESC
`, [userId]);
if (tokens.length > 0) {
await this.redis.sadd(
`push_tokens:${userId}`,
...tokens.map(t => t.token)
);
await this.redis.expire(`push_tokens:${userId}`, 86400); // 24 saat
}
return tokens;
}
}
Email Teslim Edilebilirliği ve Bounce Yönetimi#
Sağlayıcı Failover’lı Email Servisi#
Email sağlayıcıları başarısız olabilir, rate limit’e takılabilir veya teslim edilebilirlik sorunları yaşayabilir. Birden fazla sağlayıcı ve akıllı routing gereken yedekliliği sağlar:
interface EmailProvider {
sendEmail(email: EmailMessage): Promise<EmailResult>;
handleWebhook(payload: any): Promise<WebhookResult>;
getDeliverabilityScore(): Promise<number>;
}
class EmailDeliveryService {
private providers: EmailProvider[] = [];
private primaryProvider: EmailProvider;
private fallbackProviders: EmailProvider[];
constructor() {
// Sağlayıcıları öncelik sırasına göre başlat
this.providers = [
new SendGridProvider(),
new AmazonSESProvider(),
new PostmarkProvider()
];
this.primaryProvider = this.providers[0];
this.fallbackProviders = this.providers.slice(1);
}
async sendEmail(
userId: string,
notification: NotificationEvent
): Promise<EmailDeliveryResult> {
try {
// Kullanıcı email ve tercihlerini al
const user = await this.getUserWithEmailPrefs(userId);
if (!user.email || !user.emailEnabled) {
return {
success: false,
reason: 'email_disabled',
attempts: []
};
}
// Kullanıcının suppression listesinde olup olmadığını kontrol et
if (await this.isUserSuppressed(user.email)) {
return {
success: false,
reason: 'user_suppressed',
attempts: []
};
}
// Email içeriğini render et
const emailContent = await this.renderEmailContent(notification, user);
// Email mesajını hazırla
const emailMessage: EmailMessage = {
to: user.email,
from: this.getFromAddress(notification.type),
subject: emailContent.subject,
html: emailContent.html,
text: emailContent.text,
metadata: {
userId,
notificationId: notification.id,
notificationType: notification.type
},
tags: [notification.type, `user:${userId}`],
unsubscribeUrl: this.generateUnsubscribeUrl(userId, notification.type)
};
// Önce birincil sağlayıcıyı dene
let result = await this.attemptDelivery(this.primaryProvider, emailMessage);
if (!result.success) {
// Yedek sağlayıcıları dene
for (const provider of this.fallbackProviders) {
console.warn(`Primary email provider failed, trying fallback: ${provider.constructor.name}`);
result = await this.attemptDelivery(provider, emailMessage);
if (result.success) break;
}
}
// Teslimat sonucunu sakla
await this.storeDeliveryResult(notification.id, 'email', result);
return {
success: result.success,
attempts: [result],
providerId: result.providerId,
messageId: result.messageId
};
} catch (error) {
console.error('Email delivery failed:', error);
return {
success: false,
reason: 'delivery_error',
error: error.message,
attempts: []
};
}
}
private async attemptDelivery(
provider: EmailProvider,
email: EmailMessage
): Promise<EmailAttemptResult> {
const startTime = Date.now();
try {
const result = await provider.sendEmail(email);
const duration = Date.now() - startTime;
return {
providerId: provider.constructor.name,
success: result.success,
messageId: result.messageId,
duration,
response: result.response
};
} catch (error) {
const duration = Date.now() - startTime;
return {
providerId: provider.constructor.name,
success: false,
duration,
error: error.message,
shouldRetry: this.isRetryableError(error)
};
}
}
private async renderEmailContent(
notification: NotificationEvent,
user: User
): Promise<EmailContent> {
// Email template'ini al
const template = await this.templateService.getTemplate(
notification.type,
'email',
user.locale
);
// Kullanıcı verisi ve bildirim verisiyle render et
const context = {
user,
...notification.data,
unsubscribeUrl: this.generateUnsubscribeUrl(user.id, notification.type),
preferencesUrl: this.generatePreferencesUrl(user.id)
};
const rendered = await this.templateService.render(template, context);
// Gerekirse markdown'ı HTML'ye çevir
const html = this.markdownToHtml(rendered.body);
const text = this.htmlToText(html);
return {
subject: rendered.subject,
html,
text
};
}
}
Email Bounce ve Complaint Yönetimi#
Bounce, complaint ve unsubscribe event’lerini düzgün işlemek teslim edilebilirlik için kritik:
class EmailWebhookHandler {
constructor(
private db: Database,
private suppressionService: SuppressionService
) {}
async handleWebhook(
provider: string,
payload: any
): Promise<WebhookProcessResult> {
try {
const events = this.parseProviderWebhook(provider, payload);
for (const event of events) {
await this.processEmailEvent(event);
}
return { success: true, eventsProcessed: events.length };
} catch (error) {
console.error('Webhook processing failed:', error);
return { success: false, error: error.message };
}
}
private async processEmailEvent(event: EmailEvent): Promise<void> {
// Teslimat kaydını güncelle
await this.db.query(`
UPDATE notification_deliveries
SET status = $1, delivered_at = $2, error_message = $3, provider_response = $4
WHERE provider_id = $5
`, [
event.status,
event.timestamp,
event.error,
JSON.stringify(event.rawData),
event.messageId
]);
// Spesifik event tiplerini işle
switch (event.type) {
case 'bounce':
await this.handleBounce(event);
break;
case 'complaint':
await this.handleComplaint(event);
break;
case 'unsubscribe':
await this.handleUnsubscribe(event);
break;
case 'delivered':
await this.handleDelivery(event);
break;
}
}
private async handleBounce(event: EmailEvent): Promise<void> {
const bounceType = event.bounceType || 'unknown';
if (bounceType === 'permanent') {
// Kalıcı bounce eden e-postayı suppress et
await this.suppressionService.addSuppression({
email: event.recipient,
reason: 'permanent_bounce',
source: 'webhook',
metadata: {
bounceSubType: event.bounceSubType,
messageId: event.messageId
}
});
console.log(`Permanently suppressed ${event.recipient} due to hard bounce`);
} else if (bounceType === 'temporary') {
// Geçici bounce'ları takip et, eşik sonrası suppress et
const bounceCount = await this.incrementBounceCount(event.recipient);
if (bounceCount >= 5) {
await this.suppressionService.addSuppression({
email: event.recipient,
reason: 'repeated_soft_bounces',
source: 'auto_suppression',
metadata: { bounceCount }
});
console.log(`Auto-suppressed ${event.recipient} after ${bounceCount} soft bounces`);
}
}
}
private async handleComplaint(event: EmailEvent): Promise<void> {
// E-postaları spam olarak işaretleyen kullanıcıları hemen suppress et
await this.suppressionService.addSuppression({
email: event.recipient,
reason: 'spam_complaint',
source: 'webhook',
metadata: {
messageId: event.messageId,
complaintType: event.complaintSubType
}
});
// Ayrıca global suppression listesine ekle
await this.db.query(`
UPDATE users SET email_enabled = false
WHERE email = $1
`, [event.recipient]);
console.log(`Suppressed ${event.recipient} due to spam complaint`);
}
}
SMS ve Webhook Kanalları#
SMS Teslimat Servisi#
class SMSDeliveryService {
private provider: SMSProvider;
private fallbackProvider: SMSProvider;
constructor() {
this.provider = new TwilioProvider();
this.fallbackProvider = new AmazonSNSProvider();
}
async sendSMS(
userId: string,
notification: NotificationEvent
): Promise<SMSDeliveryResult> {
try {
const user = await this.getUserWithSMSPrefs(userId);
if (!user.phone || !user.smsEnabled) {
return { success: false, reason: 'sms_disabled' };
}
// SMS içeriği kısa olmalı
const content = await this.renderSMSContent(notification, user);
// Birincil sağlayıcıyı dene
let result = await this.provider.sendSMS({
to: user.phone,
message: content,
metadata: {
userId,
notificationId: notification.id
}
});
if (!result.success) {
// Yedeği dene
result = await this.fallbackProvider.sendSMS({
to: user.phone,
message: content,
metadata: { userId, notificationId: notification.id }
});
}
await this.storeDeliveryResult(notification.id, 'sms', result);
return result;
} catch (error) {
console.error('SMS delivery failed:', error);
return { success: false, error: error.message };
}
}
private async renderSMSContent(
notification: NotificationEvent,
user: User
): Promise<string> {
const template = await this.templateService.getTemplate(
notification.type,
'sms',
user.locale
);
const rendered = await this.templateService.render(template, {
user,
...notification.data
});
// SMS karakter limitleri var
return this.truncateForSMS(rendered.body, 160);
}
}
Entegrasyonlar için Webhook Teslimatı#
class WebhookDeliveryService {
private httpClient: HTTPClient;
private retryQueue: Queue;
constructor() {
this.httpClient = new HTTPClient({
timeout: 10000,
retries: 3,
retryDelay: 1000
});
}
async sendWebhook(
userId: string,
notification: NotificationEvent
): Promise<WebhookDeliveryResult> {
try {
// Kullanıcının webhook yapılandırmalarını al
const webhookConfigs = await this.getWebhookConfigs(userId, notification.type);
if (webhookConfigs.length === 0) {
return { success: false, reason: 'no_webhooks_configured' };
}
// Yapılandırılmış her webhook'a gönder
const deliveryPromises = webhookConfigs.map(config =>
this.deliverToWebhook(config, notification)
);
const results = await Promise.allSettled(deliveryPromises);
const successful = results.filter(r =>
r.status === 'fulfilled' && r.value.success
).length;
return {
success: successful > 0,
delivered: successful,
total: webhookConfigs.length,
results: results.map(r =>
r.status === 'fulfilled' ? r.value : { success: false, error: r.reason }
)
};
} catch (error) {
console.error('Webhook delivery failed:', error);
return { success: false, error: error.message };
}
}
private async deliverToWebhook(
config: WebhookConfig,
notification: NotificationEvent
): Promise<WebhookResult> {
const payload = {
id: notification.id,
type: notification.type,
userId: notification.userId,
data: notification.data,
timestamp: notification.scheduledAt || new Date(),
signature: this.generateSignature(config.secret, notification)
};
try {
const response = await this.httpClient.post(config.url, payload, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': payload.signature,
'User-Agent': 'NotificationSystem/1.0'
}
});
return {
success: response.status >= 200 && response.status < 300,
statusCode: response.status,
response: response.data
};
} catch (error) {
return {
success: false,
error: error.message,
shouldRetry: this.isRetryableHttpError(error)
};
}
}
}
Kanal Koordinasyonu ve Teslimat Mantığı#
class MultiChannelDeliveryOrchestrator {
async deliverNotification(notification: NotificationEvent): Promise<void> {
// Bu bildirim tipi için kullanıcı tercihlerini al
const preferences = await this.preferenceManager
.getEnabledChannels(notification.userId, notification.type);
if (preferences.length === 0) {
await this.analytics.trackSkipped(notification.id, 'no_enabled_channels');
return;
}
// Bildirim önceliği ve tipine göre teslimat kurallarını uygula
const deliveryPlan = this.createDeliveryPlan(notification, preferences);
// Teslimat planını çalıştır
const results = await this.executeDeliveryPlan(deliveryPlan);
// Genel teslimat başarısını takip et
await this.analytics.trackMultiChannelDelivery(notification.id, results);
}
private createDeliveryPlan(
notification: NotificationEvent,
enabledChannels: NotificationChannel[]
): DeliveryPlan {
const plan: DeliveryPlan = {
immediate: [],
delayed: [],
conditional: []
};
// Kritik bildirimler tüm kanallara hemen gider
if (notification.priority === 'critical') {
plan.immediate = enabledChannels;
return plan;
}
// Normal bildirimler kullanıcı tercihleri ve akıllı kuralları izler
for (const channel of enabledChannels) {
if (channel === 'in_app' || channel === 'push') {
plan.immediate.push(channel);
} else if (channel === 'email') {
// Email batching için geciktirilebilir
plan.delayed.push({
channel,
delay: this.getEmailBatchDelay(notification.userId)
});
} else {
plan.conditional.push({
channel,
condition: this.getDeliveryCondition(channel, notification)
});
}
}
return plan;
}
}
Baştan Hesaba Katılacak Arıza Modları#
Bağlantılar düşer, kritik durumun socket dışında tutulması gerekir; böylece yeniden bağlanma oturumu yeniden kurabilir. Push token’lar zaman zaman eskir. Geçersiz token yanıtı rutin bir temizliktir, cihaz yeniden bağlandığında tekrar kaydedilir. Email’de başarılı bir gönderim çağrısı sadece süreci başlatır. Mesajın gerçekten ulaşıp ulaşmadığına suppression listesi, bounce sınıflandırması ve ikinci bir sağlayıcının hazır olup olmadığı karar verir.
Buradaki her sağlayıcı rate limit uyguluyor; bu yüzden backoff en baştan teslimat yoluna dahil olmalı. Tercih değişiklikleri ve opt-out’lar bir sonraki gönderimde geçerli olmalı; aksi halde teslim edilebilirlik düşer. İzleme kanal başına ayrı kalıyor, çünkü bağlantı sayıları, push teslimat oranları, email bounce oranları ve SMS maliyetleri birbirinden bağımsız hareket ediyor.
Serinin bir sonraki bölümü bu kanalların üretimde debug ve izleme tarafını ele alıyor.
Kaynaklar#
- API Gateway WebSocket API’leri - AWS Belgeleri (yeni sekmede açılır) - Bildirim sistemlerinde kalıcı istemci bağlantıları için çift yönlü gerçek zamanlı WebSocket API’leri oluşturmaya resmi AWS rehberi
- Arka uç servisinde @connections komutlarını kullanma - AWS Belgeleri (yeni sekmede açılır) - Bir arka uç Lambda veya servisinden bağlı WebSocket istemcilerine mesaj iletme yöntemi
- RFC 6455 - WebSocket Protokolü - IETF (yeni sekmede açılır) - WebSocket protokolünü, çerçevelemeyi ve bağlantı yaşam döngüsünü tanımlayan temel IETF standardı
- Firebase Cloud Messaging - firebase.google.com (yeni sekmede açılır) - Konu mesajlaşma ve güvenilirlik garantileriyle Android, iOS ve web’i kapsayan çok platformlu push bildirim teslimatı
- APNs’e bildirim istekleri gönderme - Apple Developer (yeni sekmede açılır) - Kimlik doğrulama ve yeniden deneme davranışı dahil iOS cihazlarına push yüklerinin iletilmesi için Apple’ın resmi belgeleri
- Amazon SQS’te dead-letter kuyrukları - AWS Belgeleri (yeni sekmede açılır) - Başarısız mesaj teslimatlarını yakalamak ve yeniden deneme iş akışlarını tetiklemek için SQS dead-letter kuyruğu kurulumu
Ölçeklenebilir Kullanıcı Bildirim Sistemi Geliştirme
Kurumsal seviye bildirim sistemlerinin tasarımı, implementasyonu ve üretim zorluklarını kapsayan kapsamlı 4-parça serisi. Mimari ve veritabanı tasarımından gerçek zamanlı teslimat, ölçekte debugging ve performans optimizasyonuna kadar.
Bu serideki tüm yazılar
İlgili yazılar
AppSync subscription'ları yalnızca mutation ile tetiklenir. Downstream BFF olaylarını NONE veri kaynaklı bir mutation'a EventBridge ve CDK ile köprülemeyi inceliyorum.
aws · graphql · serverless +4
Tek bir backend üzerinde çalışan web SPA ve mobil uygulama için uzun süreli işlere dair tek bir varsayılan desen ve onu geçersiz kılmanız gereken durumlar.
api-design · real-time · webhooks +4
Distributed sistemlerde feature flag için production rehberi: LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması, rollout ve A/B testing örnekleri.
feature-flags · devops · ci-cd +5
AWS AppSync ile ölçeklenebilir real-time API'ler: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
aws · graphql · serverless +4
IoT lojistik için MQTT, AMQP, ZeroMQ, CoAP ve DDS'nin teknik karşılaştırması: filo takibi, soğuk zincir izleme ve gerçek zamanlı cihaz iletişimi.
messaging · industry-trends · real-time +2