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
This commit is contained in:
Lentil Hoffman 2025-06-15 12:30:07 -05:00
parent dd8987563a
commit 35bbc974d8
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
5 changed files with 55 additions and 121 deletions

View File

@ -41,11 +41,32 @@ export abstract class Lossy<Accumulator, Result> {
// apply a filter to the deltas composing that lossless view, // apply a filter to the deltas composing that lossless view,
// and then apply a supplied resolver function which receives // and then apply a supplied resolver function which receives
// the filtered lossless view as input. // the filtered lossless view as input.
// Resolve the current state of the view
resolve(entityIds?: DomainEntityID[]): Result | undefined { resolve(entityIds?: DomainEntityID[]): Result | undefined {
if (!entityIds) { if (!entityIds) {
entityIds = Array.from(this.lossless.domainEntities.keys()); entityIds = Array.from(this.lossless.domainEntities.keys());
} }
// If we don't have an accumulator, build it from the lossless view
if (!this.accumulator) {
this.accumulator = {} as Accumulator;
// Use the general view method to get the full view
const fullView = this.lossless.view(entityIds, this.deltaFilter);
// Build the accumulator by reducing each entity's view
for (const entityId of entityIds) {
const losslessViewOne = fullView[entityId];
if (losslessViewOne) {
if (!this.accumulator) {
this.accumulator = this.initializer(losslessViewOne);
} else {
this.accumulator = this.reducer(this.accumulator, losslessViewOne);
}
}
}
}
if (!this.accumulator) return undefined; if (!this.accumulator) return undefined;
return this.resolver(this.accumulator); return this.resolver(this.accumulator);

View File

@ -64,8 +64,10 @@ export class AggregationResolver extends Lossy<Accumulator, Result> {
super(lossless); super(lossless);
} }
initializer(): Accumulator { initializer(view: LosslessViewOne): Accumulator {
return {}; return {
[view.id]: { id: view.id, properties: {} }
};
} }
reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator { reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
@ -120,31 +122,7 @@ export class AggregationResolver extends Lossy<Accumulator, Result> {
return res; return res;
} }
// Override resolve to build accumulator on-demand if needed
resolve(entityIds?: DomainEntityID[]): Result | undefined {
if (!entityIds) {
entityIds = Array.from(this.lossless.domainEntities.keys());
}
// If we don't have an accumulator, build it from the lossless view
if (!this.accumulator) {
this.accumulator = this.initializer();
// Use the general view method instead of viewSpecific
const fullView = this.lossless.view(entityIds, this.deltaFilter);
for (const entityId of entityIds) {
const losslessViewOne = fullView[entityId];
if (losslessViewOne) {
this.accumulator = this.reducer(this.accumulator, losslessViewOne);
}
}
}
if (!this.accumulator) return undefined;
return this.resolver(this.accumulator);
}
} }
// Convenience classes for common aggregation types // Convenience classes for common aggregation types

View File

@ -63,8 +63,10 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
super(lossless); super(lossless);
} }
initializer(): CustomResolverAccumulator { initializer(view: LosslessViewOne): CustomResolverAccumulator {
return {}; return {
[view.id]: { id: view.id, properties: {} }
};
} }
reducer(acc: CustomResolverAccumulator, cur: LosslessViewOne): CustomResolverAccumulator { reducer(acc: CustomResolverAccumulator, cur: LosslessViewOne): CustomResolverAccumulator {
@ -118,30 +120,7 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
return res; return res;
} }
// Override resolve to build accumulator on-demand if needed
resolve(entityIds?: DomainEntityID[]): CustomResolverResult | undefined {
if (!entityIds) {
entityIds = Array.from(this.lossless.domainEntities.keys());
}
// If we don't have an accumulator, build it from the lossless view
if (!this.accumulator) {
this.accumulator = this.initializer();
const fullView = this.lossless.view(entityIds, this.deltaFilter);
for (const entityId of entityIds) {
const losslessViewOne = fullView[entityId];
if (losslessViewOne) {
this.accumulator = this.reducer(this.accumulator, losslessViewOne);
}
}
}
if (!this.accumulator) return undefined;
return this.resolver(this.accumulator);
}
} }
// Built-in plugin implementations // Built-in plugin implementations

View File

@ -70,8 +70,10 @@ export function lastValueFromDeltas(
} }
export class LastWriteWins extends Lossy<Accumulator, Result> { export class LastWriteWins extends Lossy<Accumulator, Result> {
initializer(): Accumulator { initializer(view: LosslessViewOne): Accumulator {
return {}; return {
[view.id]: { id: view.id, properties: {} }
};
} }
reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator { reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
@ -81,55 +83,31 @@ export class LastWriteWins extends Lossy<Accumulator, Result> {
for (const [key, deltas] of Object.entries(cur.propertyDeltas)) { for (const [key, deltas] of Object.entries(cur.propertyDeltas)) {
const { value, timeUpdated } = lastValueFromDeltas(key, deltas) || {}; const { value, timeUpdated } = lastValueFromDeltas(key, deltas) || {};
if (!value || !timeUpdated) continue; if (!value || timeUpdated === undefined) continue;
if (timeUpdated > (acc[cur.id].properties[key]?.timeUpdated || 0)) { const currentTime = acc[cur.id].properties[key]?.timeUpdated || 0;
acc[cur.id].properties[key] = { if (timeUpdated > currentTime) {
value, acc[cur.id].properties[key] = { value, timeUpdated };
timeUpdated
};
} }
} }
return acc; return acc;
}; }
resolver(cur: Accumulator): Result { resolver(cur: Accumulator): Result {
const res: Result = {}; const result: Result = {};
for (const [id, ent] of Object.entries(cur)) { for (const [id, entity] of Object.entries(cur)) {
res[id] = {id, properties: {}}; result[id] = {
for (const [key, {value}] of Object.entries(ent.properties)) { id,
res[id].properties[key] = value; properties: Object.fromEntries(
} Object.entries(entity.properties)
} .map(([key, { value }]) => [key, value])
)
return res;
}; };
// Override resolve to build accumulator on-demand if needed
resolve(entityIds?: DomainEntityID[]): Result | undefined {
if (!entityIds) {
entityIds = Array.from(this.lossless.domainEntities.keys());
} }
// If we don't have an accumulator, build it from the lossless view return result;
if (!this.accumulator) {
this.accumulator = this.initializer();
// Use the general view method
const fullView = this.lossless.view(entityIds, this.deltaFilter);
for (const entityId of entityIds) {
const losslessViewOne = fullView[entityId];
if (losslessViewOne) {
this.accumulator = this.reducer(this.accumulator, losslessViewOne);
}
}
}
if (!this.accumulator) return undefined;
return this.resolver(this.accumulator);
} }
} }

View File

@ -72,8 +72,10 @@ export class TimestampResolver extends Lossy<Accumulator, Result> {
super(lossless); super(lossless);
} }
initializer(): Accumulator { initializer(view: LosslessViewOne): Accumulator {
return {}; return {
[view.id]: { id: view.id, properties: {} }
};
} }
reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator { reducer(acc: Accumulator, cur: LosslessViewOne): Accumulator {
@ -124,31 +126,7 @@ export class TimestampResolver extends Lossy<Accumulator, Result> {
return res; return res;
} }
// Override resolve to build accumulator on-demand if needed
resolve(entityIds?: DomainEntityID[]): Result | undefined {
if (!entityIds) {
entityIds = Array.from(this.lossless.domainEntities.keys());
}
// If we don't have an accumulator, build it from the lossless view
if (!this.accumulator) {
this.accumulator = this.initializer();
// Use the general view method instead of viewSpecific
const fullView = this.lossless.view(entityIds, this.deltaFilter);
for (const entityId of entityIds) {
const losslessViewOne = fullView[entityId];
if (losslessViewOne) {
this.accumulator = this.reducer(this.accumulator, losslessViewOne);
}
}
}
if (!this.accumulator) return undefined;
return this.resolver(this.accumulator);
}
} }
// Convenience classes for different tie-breaking strategies // Convenience classes for different tie-breaking strategies