- Moved the resolve method implementation from individual resolvers to the base Lossy class - Updated initializer methods to accept a LosslessViewOne parameter - Removed redundant resolve methods from LastWriteWins, TimestampResolver, CustomResolver, and AggregationResolver - Ensured consistent behavior across all resolver implementations - All tests passing with the refactored code
155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
import { EntityProperties } from "../../core/entity";
|
|
import { Lossless, LosslessViewOne } from "../lossless";
|
|
import { Lossy } from '../lossy';
|
|
import { DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany } from "../../core/types";
|
|
import { valueFromCollapsedDelta } from "./last-write-wins";
|
|
|
|
export type TieBreakingStrategy = 'creator-id' | 'delta-id' | 'host-id' | 'lexicographic';
|
|
|
|
type TimestampedPropertyWithTieBreaking = {
|
|
value: PropertyTypes,
|
|
timeUpdated: Timestamp,
|
|
creator: string,
|
|
deltaId: string,
|
|
host: string
|
|
};
|
|
|
|
type TimestampedPropertiesWithTieBreaking = {
|
|
[key: PropertyID]: TimestampedPropertyWithTieBreaking
|
|
};
|
|
|
|
export type TimestampedViewOne = {
|
|
id: DomainEntityID;
|
|
properties: TimestampedPropertiesWithTieBreaking;
|
|
};
|
|
|
|
export type TimestampedViewMany = ViewMany<TimestampedViewOne>;
|
|
|
|
export type ResolvedTimestampedViewOne = {
|
|
id: DomainEntityID;
|
|
properties: EntityProperties;
|
|
};
|
|
|
|
export type ResolvedTimestampedViewMany = ViewMany<ResolvedTimestampedViewOne>;
|
|
|
|
type Accumulator = TimestampedViewMany;
|
|
type Result = ResolvedTimestampedViewMany;
|
|
|
|
function compareWithTieBreaking(
|
|
a: TimestampedPropertyWithTieBreaking,
|
|
b: TimestampedPropertyWithTieBreaking,
|
|
strategy: TieBreakingStrategy
|
|
): number {
|
|
// First compare by timestamp (most recent wins)
|
|
if (a.timeUpdated !== b.timeUpdated) {
|
|
return a.timeUpdated - b.timeUpdated;
|
|
}
|
|
|
|
// If timestamps are equal, use tie-breaking strategy
|
|
switch (strategy) {
|
|
case 'creator-id':
|
|
return a.creator.localeCompare(b.creator);
|
|
case 'delta-id':
|
|
return a.deltaId.localeCompare(b.deltaId);
|
|
case 'host-id':
|
|
return a.host.localeCompare(b.host);
|
|
case 'lexicographic':
|
|
// Compare by value if it's a string, otherwise by delta ID
|
|
if (typeof a.value === 'string' && typeof b.value === 'string') {
|
|
return a.value.localeCompare(b.value);
|
|
}
|
|
return a.deltaId.localeCompare(b.deltaId);
|
|
default:
|
|
throw new Error(`Unknown tie-breaking strategy: ${strategy}`);
|
|
}
|
|
}
|
|
|
|
export class TimestampResolver extends Lossy<Accumulator, Result> {
|
|
constructor(
|
|
lossless: Lossless,
|
|
private tieBreakingStrategy: TieBreakingStrategy = 'delta-id'
|
|
) {
|
|
super(lossless);
|
|
}
|
|
|
|
initializer(view: LosslessViewOne): Accumulator {
|
|
return {
|
|
[view.id]: { id: view.id, properties: {} }
|
|
};
|
|
}
|
|
|
|
reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
|
|
if (!acc[cur.id]) {
|
|
acc[cur.id] = { id: cur.id, properties: {} };
|
|
}
|
|
|
|
for (const [key, deltas] of Object.entries(cur.propertyDeltas)) {
|
|
let bestProperty: TimestampedPropertyWithTieBreaking | undefined;
|
|
|
|
for (const delta of deltas || []) {
|
|
const value = valueFromCollapsedDelta(key, delta);
|
|
if (value === undefined) continue;
|
|
|
|
const property: TimestampedPropertyWithTieBreaking = {
|
|
value,
|
|
timeUpdated: delta.timeCreated,
|
|
creator: delta.creator,
|
|
deltaId: delta.id,
|
|
host: delta.host
|
|
};
|
|
|
|
if (!bestProperty || compareWithTieBreaking(property, bestProperty, this.tieBreakingStrategy) > 0) {
|
|
bestProperty = property;
|
|
}
|
|
}
|
|
|
|
if (bestProperty) {
|
|
const existing = acc[cur.id].properties[key];
|
|
if (!existing || compareWithTieBreaking(bestProperty, existing, this.tieBreakingStrategy) > 0) {
|
|
acc[cur.id].properties[key] = bestProperty;
|
|
}
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
resolver(cur: Accumulator): Result {
|
|
const res: Result = {};
|
|
|
|
for (const [id, ent] of Object.entries(cur)) {
|
|
res[id] = { id, properties: {} };
|
|
for (const [key, timestampedProp] of Object.entries(ent.properties)) {
|
|
res[id].properties[key] = timestampedProp.value;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// Convenience classes for different tie-breaking strategies
|
|
export class CreatorIdTimestampResolver extends TimestampResolver {
|
|
constructor(lossless: Lossless) {
|
|
super(lossless, 'creator-id');
|
|
}
|
|
}
|
|
|
|
export class DeltaIdTimestampResolver extends TimestampResolver {
|
|
constructor(lossless: Lossless) {
|
|
super(lossless, 'delta-id');
|
|
}
|
|
}
|
|
|
|
export class HostIdTimestampResolver extends TimestampResolver {
|
|
constructor(lossless: Lossless) {
|
|
super(lossless, 'host-id');
|
|
}
|
|
}
|
|
|
|
export class LexicographicTimestampResolver extends TimestampResolver {
|
|
constructor(lossless: Lossless) {
|
|
super(lossless, 'lexicographic');
|
|
}
|
|
} |