forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpush-notifications.processor.ts
More file actions
132 lines (116 loc) · 4.5 KB
/
Copy pathpush-notifications.processor.ts
File metadata and controls
132 lines (116 loc) · 4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { Process, Processor, OnQueueFailed } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { Job } from 'bull';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import * as firebaseAdmin from 'firebase-admin';
import { DeviceRegistration } from './entities/device-registration.entity';
import { NotificationEventType } from './entities/notification-preference.entity';
import { logJobFailure } from '../common/queue-job-policy';
@Processor('push_queue')
export class PushNotificationProcessor {
private readonly logger = new Logger(PushNotificationProcessor.name);
private firebaseApp: firebaseAdmin.app.App | undefined;
constructor(
@InjectRepository(DeviceRegistration)
private deviceRepo: Repository<DeviceRegistration>,
private configService: ConfigService,
) {
this.initializeFirebase();
}
private initializeFirebase() {
const serviceAccount = this.configService.get('FIREBASE_SERVICE_ACCOUNT');
if (serviceAccount) {
try {
// Check if already initialized to avoid error
if (!firebaseAdmin.apps.length) {
this.firebaseApp = firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(JSON.parse(serviceAccount)),
});
} else {
this.firebaseApp = firebaseAdmin.app();
}
} catch (error) {
this.logger.error('Failed to initialize Firebase Admin', error);
}
} else {
this.logger.warn('Firebase service account not provided. Push notifications will not work.');
}
}
@Process('sendPush')
async handleSendPush(job: Job<{ userId: string; eventType: NotificationEventType; title: string; body: string; data?: Record<string, string> }>) {
const { userId, eventType, title, body, data } = job.data;
this.logger.debug(`Processing push notification for user ${userId} event ${eventType}`);
// Get devices
const devices = await this.deviceRepo.find({
where: { userId, isActive: true },
});
if (devices.length === 0) {
this.logger.debug(`No active devices for user ${userId}`);
return;
}
const tokens = devices.map((d) => d.deviceToken);
// Send via FCM
if (!this.firebaseApp) {
this.logger.warn('Firebase not initialized. Skipping notification send.');
return;
}
const message: firebaseAdmin.messaging.MulticastMessage = {
tokens,
notification: {
title,
body,
},
data,
android: {
priority: 'high',
},
apns: {
payload: {
aps: {
sound: 'default',
},
},
},
};
try {
const response = await this.firebaseApp.messaging().sendEachForMulticast(message);
// Handle failed tokens
if (response.failureCount > 0) {
const failedTokens: string[] = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
const error = resp.error;
if (error && (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered')) {
failedTokens.push(tokens[idx]);
}
// If error is internal or retryable, we could throw to let Bull retry the job.
// But since sendEachForMulticast is partial success, retrying the whole job might resend to successful tokens.
// Ideally we should create new job for failed tokens, but that's complex.
// For now, we accept partial success.
}
});
if (failedTokens.length > 0) {
await this.handleFailedTokens(failedTokens);
}
}
this.logger.log(`Sent push notification to ${response.successCount}/${tokens.length} devices for user ${userId}`);
} catch (error) {
logJobFailure(this.logger, job, error, { context: 'push-notification' });
throw error; // Let Bull retry
}
}
/**
* Dead-letter handler: fires when the push notification job has exhausted all retries.
*/
@OnQueueFailed()
onFailed(job: Job, err: Error) {
logJobFailure(this.logger, job, err, { context: 'push-notification-dead-letter' });
}
private async handleFailedTokens(tokens: string[]) {
this.logger.log(`Removing ${tokens.length} invalid tokens`);
await this.deviceRepo.delete({ deviceToken: In(tokens) });
}
}