// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { Constructor } from '@cratis/fundamentals'; import { IClientArtifactsProvider } from '../artifacts/index.js'; import { ChronicleConnection } from '../connection/index.js'; import { ensureCommandSuccess } from '../connection/callResults.js'; import { EventTypeId } from './EventTypeId.js'; import { IEventTypes } from './IEventTypes.js'; import { getEventTypeMetadata, getEventTypeJsonSchemaFor } from './eventTypeDecorator.js'; import { EventMigrationBuilder } from './migrations/EventMigrationBuilder.js'; import { EventTypeMigrators } from './migrations/EventTypeMigrators.js'; import { getEventTypeMigrationMetadata } from './migrations/eventTypeMigration.js'; import { IEventTypeMigration } from './migrations/IEventTypeMigration.js'; // Mirrors .NET client behavior for migration-only generations that do not have a local CLR/TS type. const UNKNOWN_GENERATION_SCHEMA = '{}'; /** * Implements {@link IEventTypes}, managing discovery and registration of event types * with the Chronicle Kernel. */ export class EventTypes implements IEventTypes { private readonly _typesByGeneration = new Map(); private readonly _latestTypes = new Map(); private readonly _eventTypeMigrators: EventTypeMigrators; /** * Creates a new {@link EventTypes} instance. * @param _eventStore - The name of the event store these types belong to. * @param _connection - The connection used to communicate with the Kernel. * @param _clientArtifacts - Provider for discovered client artifact types. */ constructor( private readonly _eventStore: string, private readonly _connection: ChronicleConnection, private readonly _clientArtifacts: IClientArtifactsProvider ) { this._eventTypeMigrators = new EventTypeMigrators(_clientArtifacts); } /** @inheritdoc */ get all(): Constructor[] { return [...this._latestTypes.values()]; } /** @inheritdoc */ async discover(): Promise { this._typesByGeneration.clear(); this._latestTypes.clear(); for (const type of this._clientArtifacts.eventTypes) { const metadata = getEventTypeMetadata(type); if (metadata) { const key = metadata.eventType.toString(); this._typesByGeneration.set(key, type); const existing = this._latestTypes.get(metadata.eventType.id.value); if (!existing) { this._latestTypes.set(metadata.eventType.id.value, type); continue; } const existingMetadata = getEventTypeMetadata(existing); if (!existingMetadata || existingMetadata.eventType.generation.value < metadata.eventType.generation.value) { this._latestTypes.set(metadata.eventType.id.value, type); } } } } /** @inheritdoc */ async register(): Promise { if (this._typesByGeneration.size === 0) { await this.discover(); } const groupedById = new Map(); for (const type of this._typesByGeneration.values()) { const metadata = getEventTypeMetadata(type); if (!metadata) { continue; } const existing = groupedById.get(metadata.eventType.id.value) ?? []; existing.push(type); groupedById.set(metadata.eventType.id.value, existing); } const registrations = Array.from(groupedById.values()).map(group => { const sorted = group .map(type => ({ type, metadata: getEventTypeMetadata(type)! })) .sort((a, b) => { const left = a.metadata.eventType.generation.value; const right = b.metadata.eventType.generation.value; if (left < right) { return -1; } if (left > right) { return 1; } return 0; }); const latest = sorted[sorted.length - 1]; const registration = { Type: { Id: latest.metadata.eventType.id.value, Generation: latest.metadata.eventType.generation.value, Tombstone: latest.metadata.eventType.tombstone }, Schema: JSON.stringify(getEventTypeJsonSchemaFor(latest.type)), Generations: sorted.map(({ type, metadata }) => ({ Generation: metadata.eventType.generation.value, Schema: JSON.stringify(getEventTypeJsonSchemaFor(type)) })), Migrations: [] as { FromGeneration: number; ToGeneration: number; UpcastJmesPath: string; DowncastJmesPath: string; }[], EventStore: this._eventStore }; for (const { type } of sorted) { const migratorTypes = this._eventTypeMigrators.getMigratorsFor(type); for (const migratorType of migratorTypes) { const metadata = getEventTypeMigrationMetadata(migratorType); if (!metadata) { continue; } const migrator = new (migratorType as Constructor)(); const upcastBuilder = new EventMigrationBuilder(); migrator.upcast(upcastBuilder); const downcastBuilder = new EventMigrationBuilder(); migrator.downcast(downcastBuilder); registration.Migrations.push({ FromGeneration: metadata.previousEventType.generation.value, ToGeneration: metadata.eventType.generation.value, UpcastJmesPath: JSON.stringify(upcastBuilder.toObject()), DowncastJmesPath: JSON.stringify(downcastBuilder.toObject()) }); if (!registration.Generations.some(_ => _.Generation === metadata.previousEventType.generation.value)) { registration.Generations.push({ Generation: metadata.previousEventType.generation.value, Schema: UNKNOWN_GENERATION_SCHEMA }); } if (!registration.Generations.some(_ => _.Generation === metadata.eventType.generation.value)) { registration.Generations.push({ Generation: metadata.eventType.generation.value, Schema: UNKNOWN_GENERATION_SCHEMA }); } } } return registration; }); if (registrations.length === 0) { return; } ensureCommandSuccess('register event types', await this._connection.eventTypes.registerEventTypes({ EventStore: this._eventStore, Types: registrations, DisableValidation: false })); } /** @inheritdoc */ hasFor(id: EventTypeId): boolean { return this._latestTypes.has(id.value); } /** @inheritdoc */ getTypeFor(id: EventTypeId): Constructor { const type = this._latestTypes.get(id.value); if (!type) { throw new Error(`No event type registered for id '${id.value}'.`); } return type; } }