import type { BusinessEvent } from '../business-events/types.ts'; import type { BeginProjectionCheckpointInput, BusinessEventDeliveryRecord, CompleteProjectionCheckpointInput, CreateProjectionDeadLetterInput, FailProjectionCheckpointInput, ProjectionCheckpointRecord, ProjectionHealthSnapshotInput, ProjectionRuntimeStore, UnsupportedProjectionCheckpointInput } from './types.ts'; export class InMemoryProjectionRuntimeStore implements ProjectionRuntimeStore { readonly deliveries = new Map(); readonly checkpoints = new Map(); readonly deadLetters: CreateProjectionDeadLetterInput[] = []; readonly healthSnapshots: ProjectionHealthSnapshotInput[] = []; async persistEvent(event: BusinessEvent): Promise { const existing = this.deliveries.get(event.eventId); if (existing) { return existing; } const record: BusinessEventDeliveryRecord = { eventId: event.eventId, organizationId: event.organizationId, eventType: event.eventType, schemaVersion: event.schemaVersion, status: 'pending', attemptCount: 0, availableAt: new Date().toISOString(), event }; this.deliveries.set(event.eventId, record); return record; } async markDeliveryProcessing(eventId: string): Promise { const delivery = this.requireDelivery(eventId); delivery.status = 'processing'; delivery.attemptCount += 1; } async markDeliveryCompleted(eventId: string): Promise { this.requireDelivery(eventId).status = 'completed'; } async markDeliveryFailed(eventId: string): Promise { this.requireDelivery(eventId).status = 'failed'; } async markDeliveryDeadLetter(eventId: string): Promise { this.requireDelivery(eventId).status = 'dead_letter'; } async getCheckpoint( consumerName: string, eventId: string ): Promise { return this.checkpoints.get(checkpointKey(consumerName, eventId)) ?? null; } async beginCheckpoint(input: BeginProjectionCheckpointInput): Promise { const key = checkpointKey(input.consumerName, input.eventId); const existing = this.checkpoints.get(key); if (existing) { existing.status = 'processing'; existing.attemptCount += 1; return existing; } const checkpoint: ProjectionCheckpointRecord = { id: crypto.randomUUID(), organizationId: input.organizationId, consumerName: input.consumerName, projectionName: input.projectionName, eventId: input.eventId, eventType: input.eventType, schemaVersion: input.schemaVersion, status: 'processing', attemptCount: 1, nextRetryAt: null, errorCode: null, errorMessage: null }; this.checkpoints.set(key, checkpoint); return checkpoint; } async completeCheckpoint(input: CompleteProjectionCheckpointInput): Promise { const checkpoint = this.requireCheckpoint(input.consumerName, input.eventId); checkpoint.status = 'completed'; checkpoint.nextRetryAt = null; checkpoint.errorCode = null; checkpoint.errorMessage = null; } async failCheckpoint(input: FailProjectionCheckpointInput): Promise { const checkpoint = this.checkpoints.get(checkpointKey(input.consumerName, input.eventId)) ?? (await this.beginCheckpoint({ ...input, now: input.now })); checkpoint.status = input.deadLetter ? 'dead_letter' : 'retry_scheduled'; checkpoint.nextRetryAt = input.nextRetryAt?.toISOString() ?? null; checkpoint.errorCode = input.failure.code; checkpoint.errorMessage = input.failure.message; } async skipUnsupportedVersion(input: UnsupportedProjectionCheckpointInput): Promise { this.checkpoints.set(checkpointKey(input.consumerName, input.eventId), { id: crypto.randomUUID(), organizationId: input.organizationId, consumerName: input.consumerName, projectionName: input.projectionName, eventId: input.eventId, eventType: input.eventType, schemaVersion: input.schemaVersion, status: 'skipped_unsupported_version', attemptCount: 0, nextRetryAt: null, errorCode: 'unsupported_event_version', errorMessage: `Unsupported schema version ${input.schemaVersion}` }); } async createDeadLetter(input: CreateProjectionDeadLetterInput): Promise { this.deadLetters.push(input); } async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise { this.healthSnapshots.push(input); } private requireDelivery(eventId: string): BusinessEventDeliveryRecord { const delivery = this.deliveries.get(eventId); if (!delivery) { throw new Error(`Missing delivery record for event ${eventId}`); } return delivery; } private requireCheckpoint(consumerName: string, eventId: string): ProjectionCheckpointRecord { const checkpoint = this.checkpoints.get(checkpointKey(consumerName, eventId)); if (!checkpoint) { throw new Error(`Missing checkpoint for ${consumerName}:${eventId}`); } return checkpoint; } } function checkpointKey(consumerName: string, eventId: string): string { return `${consumerName}:${eventId}`; }