rhizome-node/src/views/resolvers/aggregation-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

167 lines
4.6 KiB
TypeScript

import { EntityProperties } from "../../core/entity";
import { Lossless, LosslessViewOne } from "../lossless";
import { Lossy } from '../lossy';
import { DomainEntityID, PropertyID, ViewMany } from "../../core/types";
import { valueFromCollapsedDelta } from "./last-write-wins";
export type AggregationType = 'min' | 'max' | 'sum' | 'average' | 'count';
export type AggregationConfig = {
[propertyId: PropertyID]: AggregationType;
};
type AggregatedProperty = {
values: number[];
type: AggregationType;
result?: number;
};
type AggregatedProperties = {
[key: PropertyID]: AggregatedProperty;
};
export type AggregatedViewOne = {
id: DomainEntityID;
properties: AggregatedProperties;
};
export type AggregatedViewMany = ViewMany<AggregatedViewOne>;
type ResolvedAggregatedViewOne = {
id: DomainEntityID;
properties: EntityProperties;
};
type ResolvedAggregatedViewMany = ViewMany<ResolvedAggregatedViewOne>;
type Accumulator = AggregatedViewMany;
type Result = ResolvedAggregatedViewMany;
function aggregateValues(values: number[], type: AggregationType): number {
if (values.length === 0) return 0;
switch (type) {
case 'min':
return Math.min(...values);
case 'max':
return Math.max(...values);
case 'sum':
return values.reduce((sum, val) => sum + val, 0);
case 'average':
return values.reduce((sum, val) => sum + val, 0) / values.length;
case 'count':
return values.length;
default:
throw new Error(`Unknown aggregation type: ${type}`);
}
}
export class AggregationResolver extends Lossy<Accumulator, Result> {
constructor(
lossless: Lossless,
private config: AggregationConfig
) {
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 [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
const aggregationType = this.config[propertyId];
if (!aggregationType) continue;
if (!acc[cur.id].properties[propertyId]) {
acc[cur.id].properties[propertyId] = {
values: [],
type: aggregationType
};
}
// Extract numeric values from all deltas for this property
const newValues: number[] = [];
for (const delta of deltas || []) {
const value = valueFromCollapsedDelta(propertyId, delta);
if (typeof value === 'number') {
newValues.push(value);
}
}
// Update the values array (avoiding duplicates by clearing and rebuilding)
acc[cur.id].properties[propertyId].values = newValues;
}
return acc;
}
resolver(cur: Accumulator): Result {
const res: Result = {};
for (const [id, entity] of Object.entries(cur)) {
const entityResult: ResolvedAggregatedViewOne = { id, properties: {} };
for (const [propertyId, aggregatedProp] of Object.entries(entity.properties)) {
const result = aggregateValues(aggregatedProp.values, aggregatedProp.type);
entityResult.properties[propertyId] = result;
}
// Only include entities that have at least one aggregated property
if (Object.keys(entityResult.properties).length > 0) {
res[id] = entityResult;
}
}
return res;
}
}
// Convenience classes for common aggregation types
export class MinResolver extends AggregationResolver {
constructor(lossless: Lossless, properties: PropertyID[]) {
const config: AggregationConfig = {};
properties.forEach(prop => config[prop] = 'min');
super(lossless, config);
}
}
export class MaxResolver extends AggregationResolver {
constructor(lossless: Lossless, properties: PropertyID[]) {
const config: AggregationConfig = {};
properties.forEach(prop => config[prop] = 'max');
super(lossless, config);
}
}
export class SumResolver extends AggregationResolver {
constructor(lossless: Lossless, properties: PropertyID[]) {
const config: AggregationConfig = {};
properties.forEach(prop => config[prop] = 'sum');
super(lossless, config);
}
}
export class AverageResolver extends AggregationResolver {
constructor(lossless: Lossless, properties: PropertyID[]) {
const config: AggregationConfig = {};
properties.forEach(prop => config[prop] = 'average');
super(lossless, config);
}
}
export class CountResolver extends AggregationResolver {
constructor(lossless: Lossless, properties: PropertyID[]) {
const config: AggregationConfig = {};
properties.forEach(prop => config[prop] = 'count');
super(lossless, config);
}
}