279 lines
7.7 KiB
TypeScript
279 lines
7.7 KiB
TypeScript
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<T = unknown> {
|
|
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<CustomResolverAccumulator, CustomResolverResult> {
|
|
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<PropertyTypes, number> }> {
|
|
name = 'majority-vote';
|
|
|
|
initialize() {
|
|
return { votes: new Map() };
|
|
}
|
|
|
|
update(currentState: { votes: Map<PropertyTypes, number> }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
|
const currentCount = currentState.votes.get(newValue) || 0;
|
|
currentState.votes.set(newValue, currentCount + 1);
|
|
return currentState;
|
|
}
|
|
|
|
resolve(state: { votes: Map<PropertyTypes, number> }): 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;
|
|
}
|
|
} |