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:
2026-07-03 02:44:23 +05:30
parent 1997fe48bb
commit 2424739e3a
11 changed files with 84 additions and 12 deletions
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { resetDb } from '../test-utils/reset-db';
import { ProjectionCursorService } from '../projection/projection-cursor.service';
import { makeFakePorts, makeFakeInference } from '@insignia/iios-testkit';
import { IIOS_EVENTS, type CloudEvent } from '@insignia/iios-contracts';
import { ActorResolver } from '../identity/actor.resolver';
@@ -48,7 +49,7 @@ beforeEach(async () => { await resetDb(prisma); });
describe('chaos: poison consumer → DLQ → replay (P9)', () => {
it('a consumer failure is dead-lettered (not swallowed), the lane keeps flowing, and replay resolves it', async () => {
const dlq = new DlqService(asService);
const projector = new AiProjector(asService, new OutboxBus(), dlq, ai());
const projector = new AiProjector(asService, new OutboxBus(), dlq, ai(), new ProjectionCursorService(asService));
dlq.registerHandler('ai-projector', (e) => projector.onNormalized(e)); // healthy handler for replay
const poison = await ingestInbound('There is a party at 9 PM tonight!');