forked from InsurNiffy/niff-Stellar-shurance
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclaim-events.service.ts
More file actions
133 lines (115 loc) · 4.46 KB
/
Copy pathclaim-events.service.ts
File metadata and controls
133 lines (115 loc) · 4.46 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
133
/**
* ClaimEventsService
*
* Bridges the Redis pub/sub channel `claim:status:changed` to active SSE
* connections via the SseConnectionRegistry.
*
* Internal bus design:
* The indexer's handleClaimFiled / handleClaimProcessed / handleVoteCast
* methods call ClaimEventsService.publish() after each Prisma upsert. This
* keeps the SSE bus decoupled from the Prisma transaction — the transaction
* commits first, then the event is published. No DB transaction is held open
* during SSE delivery.
*
* In a multi-instance deployment, each backend instance subscribes to the
* same Redis channel. When any instance calls publish(), all instances
* receive the message and broadcast to their local SSE connections. This
* ensures horizontal scale without a centralised fan-out process.
*
* Redis channel: `claim:status:changed`
* Message format: JSON { claimId: string; status: string; updatedAt: string; ledger?: number }
*/
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import Redis from "ioredis";
import { ConfigService } from "@nestjs/config";
import { SseConnectionRegistry } from "./sse-connection.registry";
export interface ClaimStatusChangedEvent {
claimId: string;
status: string;
updatedAt: string;
ledger?: number;
}
const REDIS_CHANNEL = "claim:status:changed";
@Injectable()
export class ClaimEventsService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(ClaimEventsService.name);
private subscriber: Redis | null = null;
private publisher: Redis | null = null;
constructor(
private readonly config: ConfigService,
private readonly registry: SseConnectionRegistry,
) {}
async onModuleInit(): Promise<void> {
const redisUrl = this.config.get<string>("REDIS_URL", "redis://localhost:6379");
// Dedicated subscriber connection — subscribe() blocks the connection for
// pub/sub mode, so it cannot be shared with the general RedisService client.
this.subscriber = new Redis(redisUrl, {
lazyConnect: true,
retryStrategy: (times) => (times > 5 ? null : Math.min(times * 200, 3000)),
});
// Dedicated publisher connection
this.publisher = new Redis(redisUrl, {
lazyConnect: true,
retryStrategy: (times) => (times > 5 ? null : Math.min(times * 200, 3000)),
});
this.subscriber.on("error", (err) =>
this.logger.warn(`SSE Redis subscriber error: ${err.message}`),
);
this.publisher.on("error", (err) =>
this.logger.warn(`SSE Redis publisher error: ${err.message}`),
);
try {
await this.subscriber.connect();
await this.publisher.connect();
await this.subscriber.subscribe(REDIS_CHANNEL);
this.logger.log(`Subscribed to Redis channel: ${REDIS_CHANNEL}`);
this.subscriber.on("message", (_channel: string, message: string) => {
this.handleMessage(message);
});
} catch (err) {
// Non-fatal at startup: SSE will not push events but the rest of the
// application remains functional. Log a clear warning so operators notice.
this.logger.warn(
`ClaimEventsService: Redis unavailable at startup. SSE push disabled. Error: ${err}`,
);
}
}
async onModuleDestroy(): Promise<void> {
this.registry.drainAll();
try {
await this.subscriber?.unsubscribe(REDIS_CHANNEL);
await this.subscriber?.quit();
await this.publisher?.quit();
} catch {
// Ignore shutdown errors
}
}
/**
* Publish a claim status change to the Redis channel.
* Call this from the indexer after a successful Prisma upsert.
* The Prisma transaction must be committed before calling this method.
*/
async publish(event: ClaimStatusChangedEvent): Promise<void> {
if (!this.publisher) return;
try {
await this.publisher.publish(REDIS_CHANNEL, JSON.stringify(event));
} catch (err) {
// Fail silently — pub/sub failure does not affect data integrity
this.logger.warn(`Failed to publish claim event: ${err}`);
}
}
private handleMessage(message: string): void {
let event: ClaimStatusChangedEvent;
try {
event = JSON.parse(message) as ClaimStatusChangedEvent;
} catch {
this.logger.warn(`Received malformed message on ${REDIS_CHANNEL}: ${message}`);
return;
}
if (!event.claimId || !event.status) {
this.logger.warn(`Ignoring incomplete claim event: ${message}`);
return;
}
this.registry.broadcast(event.claimId, event);
}
}