Files
alla-allaos-fullstack/src/features/foundation/projections/memory-store.ts
phaichayon 06af36156f task ep.1.4
2026-07-13 12:48:33 +07:00

159 lines
5.2 KiB
TypeScript

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<string, BusinessEventDeliveryRecord>();
readonly checkpoints = new Map<string, ProjectionCheckpointRecord>();
readonly deadLetters: CreateProjectionDeadLetterInput[] = [];
readonly healthSnapshots: ProjectionHealthSnapshotInput[] = [];
async persistEvent(event: BusinessEvent): Promise<BusinessEventDeliveryRecord> {
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<void> {
const delivery = this.requireDelivery(eventId);
delivery.status = 'processing';
delivery.attemptCount += 1;
}
async markDeliveryCompleted(eventId: string): Promise<void> {
this.requireDelivery(eventId).status = 'completed';
}
async markDeliveryFailed(eventId: string): Promise<void> {
this.requireDelivery(eventId).status = 'failed';
}
async markDeliveryDeadLetter(eventId: string): Promise<void> {
this.requireDelivery(eventId).status = 'dead_letter';
}
async getCheckpoint(
consumerName: string,
eventId: string
): Promise<ProjectionCheckpointRecord | null> {
return this.checkpoints.get(checkpointKey(consumerName, eventId)) ?? null;
}
async beginCheckpoint(input: BeginProjectionCheckpointInput): Promise<ProjectionCheckpointRecord> {
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<void> {
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<void> {
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<void> {
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<void> {
this.deadLetters.push(input);
}
async recordHealthSnapshot(input: ProjectionHealthSnapshotInput): Promise<void> {
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}`;
}