rhizome-node/src/views/resolvers/timestamp-resolvers.ts
Lentil Hoffman 35bbc974d8
refactor: move common resolve logic to base Lossy class
- 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
2025-06-15 14:15:48 -05:00

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');
}
}