task ep.1.4
This commit is contained in:
158
src/features/foundation/projections/memory-store.ts
Normal file
158
src/features/foundation/projections/memory-store.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user