11import { Repository } from "typeorm" ;
22import { Verification } from "../entities/Verification" ;
33import { Notification } from "../entities/Notification" ;
4+ import { DeviceToken } from "../entities/DeviceToken" ;
45
56export interface DeviceRegistration {
67 eName : string ;
@@ -22,10 +23,27 @@ export interface SendNotificationRequest {
2223 sharedSecret : string ;
2324}
2425
26+ const BAD_TOKEN_ERRORS = [
27+ "messaging/registration-token-not-valid" ,
28+ "messaging/invalid-registration-token" ,
29+ "messaging/mismatched-credential" ,
30+ "BadDeviceToken" ,
31+ "Unregistered" ,
32+ "DeviceTokenNotForTopic" ,
33+ "ExpiredProviderToken" ,
34+ "InvalidProviderToken" ,
35+ ] ;
36+
37+ function isBadTokenError ( error : unknown ) : boolean {
38+ const msg = error instanceof Error ? error . message : String ( error ) ;
39+ return BAD_TOKEN_ERRORS . some ( ( e ) => msg . includes ( e ) ) ;
40+ }
41+
2542export class NotificationService {
2643 constructor (
2744 private verificationRepository : Repository < Verification > ,
28- private notificationRepository : Repository < Notification >
45+ private notificationRepository : Repository < Notification > ,
46+ private deviceTokenRepository ?: Repository < DeviceToken > ,
2947 ) { }
3048
3149 async registerDevice ( registration : DeviceRegistration ) : Promise < Verification > {
@@ -56,10 +74,10 @@ export class NotificationService {
5674 if ( verification ) {
5775 verification . platform = registration . platform ;
5876 if ( token ) {
59- const existing = verification . pushTokens ?? [ ] ;
60- if ( ! existing . includes ( token ) ) {
61- verification . pushTokens = [ ... existing , token ] ;
62- }
77+ // Replace all tokens for this device — the latest token from the
78+ // OS is the only valid one. Appending caused stale tokens to
79+ // accumulate and never get cleaned up.
80+ verification . pushTokens = [ token ] ;
6381 }
6482 verification . deviceActive = true ;
6583 verification . updatedAt = new Date ( ) ;
@@ -174,20 +192,71 @@ export class NotificationService {
174192
175193 const pushSucceeded = pushResults . filter ( r => r . status === "fulfilled" ) . length ;
176194 const pushFailed = pushResults . filter ( r => r . status === "rejected" ) . length ;
195+
196+ // Collect tokens that returned a known "bad token" error so we can purge them
197+ const badTokens : string [ ] = [ ] ;
198+ pushResults . forEach ( ( r , i ) => {
199+ if ( r . status === "rejected" ) {
200+ console . error ( `[NOTIF] Push failed for token index ${ i } :` , r . reason ) ;
201+ if ( isBadTokenError ( r . reason ) ) {
202+ badTokens . push ( allTokens [ i ] . token ) ;
203+ }
204+ }
205+ } ) ;
206+
177207 if ( pushFailed > 0 ) {
178208 console . log ( `[NOTIF] Push results for ${ eName } : ${ pushSucceeded } sent, ${ pushFailed } failed` ) ;
179- pushResults . forEach ( ( r , i ) => {
180- if ( r . status === "rejected" ) {
181- console . error ( `[NOTIF] Push failed for token index ${ i } :` , r . reason ) ;
182- }
183- } ) ;
184209 } else {
185210 console . log ( `[NOTIF] Push sent successfully to ${ pushSucceeded } token(s) for ${ eName } ` ) ;
186211 }
187212
213+ // Purge bad tokens from both Verification and DeviceToken tables
214+ if ( badTokens . length > 0 ) {
215+ console . log ( `[NOTIF] Removing ${ badTokens . length } bad token(s) for ${ eName } ` ) ;
216+ await this . removeBadTokens ( eName , badTokens ) ;
217+ }
218+
188219 return pushSucceeded > 0 || pushFailed === 0 ;
189220 }
190221
222+ private async removeBadTokens ( eName : string , badTokens : string [ ] ) : Promise < void > {
223+ try {
224+ // Clean Verification table
225+ const verifications = await this . verificationRepository . find ( {
226+ where : { linkedEName : eName } ,
227+ } ) ;
228+ for ( const v of verifications ) {
229+ const before = v . pushTokens ?. length ?? 0 ;
230+ v . pushTokens = ( v . pushTokens ?? [ ] ) . filter ( ( t ) => ! badTokens . includes ( t ) ) ;
231+ if ( v . pushTokens . length !== before ) {
232+ v . updatedAt = new Date ( ) ;
233+ await this . verificationRepository . save ( v ) ;
234+ }
235+ }
236+
237+ // Clean DeviceToken table
238+ if ( this . deviceTokenRepository ) {
239+ const normalized = eName . startsWith ( "@" ) ? eName : `@${ eName } ` ;
240+ const withoutAt = eName . replace ( / ^ @ / , "" ) ;
241+ const rows = await this . deviceTokenRepository
242+ . createQueryBuilder ( "dt" )
243+ . where ( "dt.eName = :e1 OR dt.eName = :e2" , { e1 : normalized , e2 : withoutAt } )
244+ . getMany ( ) ;
245+
246+ for ( const row of rows ) {
247+ const before = row . tokens . length ;
248+ row . tokens = row . tokens . filter ( ( t ) => ! badTokens . includes ( t ) ) ;
249+ if ( row . tokens . length !== before ) {
250+ row . updatedAt = new Date ( ) ;
251+ await this . deviceTokenRepository . save ( row ) ;
252+ }
253+ }
254+ }
255+ } catch ( err ) {
256+ console . error ( `[NOTIF] Failed to remove bad tokens for ${ eName } :` , err ) ;
257+ }
258+ }
259+
191260 async getUndeliveredNotifications ( eName : string ) : Promise < Notification [ ] > {
192261 return await this . notificationRepository . find ( {
193262 where : { eName, delivered : false } ,
0 commit comments