import { EntityProperties } from "../../core/entity"; import { CollapsedDelta, Lossless, LosslessViewOne } from "../lossless"; import { Lossy } from '../lossy'; import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../core/types"; // Plugin interface for custom resolvers export interface ResolverPlugin { name: string; // Initialize the state for a property initialize(): T; // Process a new value for the property update(currentState: T, newValue: PropertyTypes, delta: CollapsedDelta): T; // Resolve the final value from the accumulated state // Returns undefined if no valid value could be resolved resolve(state: T): PropertyTypes | undefined; } // Configuration for custom resolver export type CustomResolverConfig = { [propertyId: PropertyID]: ResolverPlugin; }; type PropertyState = { plugin: ResolverPlugin; state: unknown; }; type EntityState = { [propertyId: PropertyID]: PropertyState; }; type CustomResolverAccumulator = { [entityId: DomainEntityID]: { id: DomainEntityID; properties: EntityState; }; }; type CustomResolverResult = ViewMany<{ id: DomainEntityID; properties: EntityProperties; }>; // Extract value from delta for a specific property function extractValueFromDelta(propertyId: PropertyID, delta: CollapsedDelta): PropertyTypes | undefined { for (const pointer of delta.pointers) { for (const [key, value] of Object.entries(pointer)) { if (key === propertyId && (typeof value === "string" || typeof value === "number")) { return value; } } } return undefined; } export class CustomResolver extends Lossy { constructor( lossless: Lossless, private config: CustomResolverConfig ) { super(lossless); } initializer(view: LosslessViewOne): CustomResolverAccumulator { return { [view.id]: { id: view.id, properties: {} } }; } reducer(acc: CustomResolverAccumulator, cur: LosslessViewOne): CustomResolverAccumulator { if (!acc[cur.id]) { acc[cur.id] = { id: cur.id, properties: {} }; } for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) { const plugin = this.config[propertyId]; if (!plugin) continue; // Initialize property state if not exists if (!acc[cur.id].properties[propertyId]) { acc[cur.id].properties[propertyId] = { plugin, state: plugin.initialize() }; } const propertyState = acc[cur.id].properties[propertyId]; // Process all deltas for this property for (const delta of deltas || []) { const value = extractValueFromDelta(propertyId, delta); if (value !== undefined) { propertyState.state = propertyState.plugin.update(propertyState.state, value, delta); } } } return acc; } resolver(cur: CustomResolverAccumulator): CustomResolverResult { const res: CustomResolverResult = {}; for (const [entityId, entity] of Object.entries(cur)) { const entityResult: { id: string; properties: EntityProperties } = { id: entityId, properties: {} }; for (const [propertyId, propertyState] of Object.entries(entity.properties)) { const resolvedValue = propertyState.plugin.resolve(propertyState.state); // Only add the property if the resolved value is not undefined if (resolvedValue !== undefined) { entityResult.properties[propertyId] = resolvedValue; } } // Only include entities that have at least one resolved property if (Object.keys(entityResult.properties).length > 0) { res[entityId] = entityResult; } } return res; } } // Built-in plugin implementations // Last Write Wins plugin export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> { name = 'last-write-wins'; initialize() { return { timestamp: 0 }; } update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) { if (delta.timeCreated > currentState.timestamp) { return { value: newValue, timestamp: delta.timeCreated }; } return currentState; } resolve(state: { value?: PropertyTypes, timestamp: number }): PropertyTypes { return state.value || ''; } } // First Write Wins plugin export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> { name = 'first-write-wins'; initialize() { return { timestamp: Infinity }; } update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) { if (delta.timeCreated < currentState.timestamp) { return { value: newValue, timestamp: delta.timeCreated }; } return currentState; } resolve(state: { value?: PropertyTypes, timestamp: number }): PropertyTypes { return state.value || ''; } } // Concatenation plugin (for string values) export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: string, timestamp: number }[] }> { name = 'concatenation'; constructor(private separator: string = ' ') { } initialize() { return { values: [] }; } update(currentState: { values: { value: string, timestamp: number }[] }, newValue: PropertyTypes, delta: CollapsedDelta) { if (typeof newValue === 'string') { // Check if this value already exists (avoid duplicates) const exists = currentState.values.some(v => v.value === newValue); if (!exists) { currentState.values.push({ value: newValue, timestamp: delta.timeCreated }); // Sort by timestamp to maintain chronological order currentState.values.sort((a, b) => a.timestamp - b.timestamp); } } return currentState; } resolve(state: { values: { value: string, timestamp: number }[] }): PropertyTypes { return state.values.map(v => v.value).join(this.separator); } } // Majority vote plugin export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map }> { name = 'majority-vote'; initialize() { return { votes: new Map() }; } update(currentState: { votes: Map }, newValue: PropertyTypes, _delta: CollapsedDelta) { const currentCount = currentState.votes.get(newValue) || 0; currentState.votes.set(newValue, currentCount + 1); return currentState; } resolve(state: { votes: Map }): PropertyTypes { let maxVotes = 0; let winner: PropertyTypes = ''; for (const [value, votes] of state.votes.entries()) { if (votes > maxVotes) { maxVotes = votes; winner = value; } } return winner; } } // Numeric min/max plugins export class MinPlugin implements ResolverPlugin<{ min?: number }> { name = 'min'; initialize() { return {}; } update(currentState: { min?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) { if (typeof newValue === 'number') { if (currentState.min === undefined || newValue < currentState.min) { return { min: newValue }; } } return currentState; } resolve(state: { min?: number }): PropertyTypes | undefined { return state.min; } } export class MaxPlugin implements ResolverPlugin<{ max?: number }> { name = 'max'; initialize() { return {}; } update(currentState: { max?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) { if (typeof newValue === 'number') { if (currentState.max === undefined || newValue > currentState.max) { return { max: newValue }; } } return currentState; } resolve(state: { max?: number }): PropertyTypes | undefined { return state.max; } }