feat(iios): projectors checkpoint the projection cursor (P9)
All 5 projectors (route/ai/inbox/calendar/raw-event) now advance their IiosProjectionCursor after a freshly-claimed event is applied — split into claim → apply → advance so a duplicate never double-counts and a throw leaves the cursor untouched (event replays via the DLQ). Inbox's two topics get two cursor rows. Proves KG-06 replay reproduction at the projection level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { OutboxBus } from '../outbox/outbox.bus';
|
||||
import { DlqService } from '../outbox/dlq.service';
|
||||
import { ProjectionCursorService } from '../projection/projection-cursor.service';
|
||||
|
||||
interface MessageSentData {
|
||||
interactionId: string;
|
||||
@@ -30,6 +31,7 @@ export class InboxProjector implements OnModuleInit {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly bus: OutboxBus,
|
||||
private readonly dlq: DlqService,
|
||||
private readonly cursor: ProjectionCursorService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
@@ -39,7 +41,12 @@ export class InboxProjector implements OnModuleInit {
|
||||
}
|
||||
|
||||
async onMessageSent(event: CloudEvent): Promise<void> {
|
||||
if (!(await this.claim(event.id))) return;
|
||||
if (!(await this.claim(event.id))) return; // duplicate → no apply, no cursor advance
|
||||
await this.applyMessageSent(event);
|
||||
await this.cursor.advance(this.consumer, event);
|
||||
}
|
||||
|
||||
private async applyMessageSent(event: CloudEvent): Promise<void> {
|
||||
const data = event.data as MessageSentData;
|
||||
const traceId = event.insignia?.correlationId;
|
||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: data.threadId } });
|
||||
@@ -53,7 +60,12 @@ export class InboxProjector implements OnModuleInit {
|
||||
}
|
||||
|
||||
async onMessageRead(event: CloudEvent): Promise<void> {
|
||||
if (!(await this.claim(event.id))) return;
|
||||
if (!(await this.claim(event.id))) return; // duplicate → no apply, no cursor advance
|
||||
await this.applyMessageRead(event);
|
||||
await this.cursor.advance(this.consumer, event);
|
||||
}
|
||||
|
||||
private async applyMessageRead(event: CloudEvent): Promise<void> {
|
||||
const data = event.data as MessageReadData;
|
||||
const item = await this.prisma.iiosInboxItem.findFirst({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user