Revert changes to CustomResolver
This commit is contained in:
parent
07c8da3cea
commit
65a6230959
@ -436,95 +436,6 @@ describe('Custom Resolvers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Plugin Communication', () => {
|
|
||||||
test('plugins should be able to access each other\'s states', () => {
|
|
||||||
// Create a plugin that depends on another property's value
|
|
||||||
class DependentPlugin implements ResolverPlugin<{ value?: string }> {
|
|
||||||
name = 'dependent';
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
return { value: 'initial' };
|
|
||||||
}
|
|
||||||
|
|
||||||
update(
|
|
||||||
currentState: { value?: string },
|
|
||||||
_newValue: PropertyTypes,
|
|
||||||
_delta: CollapsedDelta,
|
|
||||||
context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
// This plugin's value depends on the 'source' property's resolved value
|
|
||||||
const sourceValue = context?.resolvedValues['source'];
|
|
||||||
return {
|
|
||||||
value: typeof sourceValue === 'string' ? `Processed: ${sourceValue}` : currentState.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(
|
|
||||||
state: { value?: string },
|
|
||||||
context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
): PropertyTypes | undefined {
|
|
||||||
// In resolve, we can also check the context if needed
|
|
||||||
const sourceValue = context?.resolvedValues['source'];
|
|
||||||
if (typeof sourceValue === 'string' && state.value === 'initial') {
|
|
||||||
return `Processed: ${sourceValue}`;
|
|
||||||
}
|
|
||||||
return state.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a resolver with both plugins
|
|
||||||
const resolver = new CustomResolver(lossless, {
|
|
||||||
source: new LastWriteWinsPlugin(),
|
|
||||||
dependent: new DependentPlugin()
|
|
||||||
});
|
|
||||||
|
|
||||||
// First, set up the source property
|
|
||||||
const sourceDelta = createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(1000)
|
|
||||||
.setProperty('entity1', 'source', 'original', 'collection')
|
|
||||||
.buildV1();
|
|
||||||
|
|
||||||
lossless.ingestDelta(sourceDelta);
|
|
||||||
|
|
||||||
// Then set up the dependent property
|
|
||||||
const dependentDelta = createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(2000)
|
|
||||||
.setProperty('entity1', 'dependent', 'initial', 'collection')
|
|
||||||
.buildV1();
|
|
||||||
|
|
||||||
lossless.ingestDelta(dependentDelta);
|
|
||||||
|
|
||||||
// Get the first result
|
|
||||||
const result = resolver.resolve();
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
// The dependent plugin should see the source value
|
|
||||||
expect(result!['entity1'].properties).toMatchObject({
|
|
||||||
source: 'original',
|
|
||||||
dependent: expect.stringContaining('Processed: original')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new delta that updates the source property
|
|
||||||
const updateDelta = createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(3000)
|
|
||||||
.setProperty('entity1', 'source', 'updated', 'collection')
|
|
||||||
.buildV1();
|
|
||||||
|
|
||||||
// Ingest the update delta
|
|
||||||
lossless.ingestDelta(updateDelta);
|
|
||||||
|
|
||||||
// Get the updated result
|
|
||||||
const updatedResult = resolver.resolve();
|
|
||||||
expect(updatedResult).toBeDefined();
|
|
||||||
|
|
||||||
// The dependent plugin should see the updated source value
|
|
||||||
expect(updatedResult!['entity1'].properties).toMatchObject({
|
|
||||||
source: 'updated',
|
|
||||||
dependent: expect.stringContaining('Processed: updated')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
test('should handle empty delta sets', () => {
|
test('should handle empty delta sets', () => {
|
||||||
const resolver = new CustomResolver(lossless, {
|
const resolver = new CustomResolver(lossless, {
|
||||||
|
@ -11,30 +11,11 @@ export interface ResolverPlugin<T = unknown> {
|
|||||||
initialize(): T;
|
initialize(): T;
|
||||||
|
|
||||||
// Process a new value for the property
|
// Process a new value for the property
|
||||||
update(
|
update(currentState: T, newValue: PropertyTypes, delta: CollapsedDelta): T;
|
||||||
currentState: T,
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
delta: CollapsedDelta,
|
|
||||||
// Additional context including other properties' states
|
|
||||||
context?: {
|
|
||||||
// Current state of all properties for the entity
|
|
||||||
entityState: Record<string, unknown>;
|
|
||||||
// Current resolved values of all properties for the entity
|
|
||||||
resolvedValues: Record<string, PropertyTypes>;
|
|
||||||
}
|
|
||||||
): T;
|
|
||||||
|
|
||||||
// Resolve the final value from the accumulated state
|
// Resolve the final value from the accumulated state
|
||||||
resolve(
|
// Returns undefined if no valid value could be resolved
|
||||||
state: T,
|
resolve(state: T): PropertyTypes | undefined;
|
||||||
// Additional context including other properties' states
|
|
||||||
context?: {
|
|
||||||
// Current state of all properties for the entity
|
|
||||||
entityState: Record<string, unknown>;
|
|
||||||
// Current resolved values of all properties for the entity
|
|
||||||
resolvedValues: Record<string, PropertyTypes>;
|
|
||||||
}
|
|
||||||
): PropertyTypes | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration for custom resolver
|
// Configuration for custom resolver
|
||||||
@ -94,15 +75,11 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
|||||||
acc[cur.id] = { id: cur.id, properties: {} };
|
acc[cur.id] = { id: cur.id, properties: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// First pass: collect all property states and resolved values
|
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
||||||
const entityState: Record<string, unknown> = {};
|
|
||||||
const resolvedValues: Record<string, PropertyTypes> = {};
|
|
||||||
|
|
||||||
// Initialize all properties first
|
|
||||||
for (const propertyId of Object.keys(cur.propertyDeltas)) {
|
|
||||||
const plugin = this.config[propertyId];
|
const plugin = this.config[propertyId];
|
||||||
if (!plugin) continue;
|
if (!plugin) continue;
|
||||||
|
|
||||||
|
// Initialize property state if not exists
|
||||||
if (!acc[cur.id].properties[propertyId]) {
|
if (!acc[cur.id].properties[propertyId]) {
|
||||||
acc[cur.id].properties[propertyId] = {
|
acc[cur.id].properties[propertyId] = {
|
||||||
plugin,
|
plugin,
|
||||||
@ -110,51 +87,13 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the current state
|
|
||||||
entityState[propertyId] = acc[cur.id].properties[propertyId].state;
|
|
||||||
|
|
||||||
// Resolve current value if possible
|
|
||||||
try {
|
|
||||||
const resolved = plugin.resolve(acc[cur.id].properties[propertyId].state, {
|
|
||||||
entityState: {},
|
|
||||||
resolvedValues: {}
|
|
||||||
});
|
|
||||||
if (resolved !== undefined) {
|
|
||||||
resolvedValues[propertyId] = resolved;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore resolution errors during reduction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: process deltas with full context
|
|
||||||
for (const [propertyId, deltas] of Object.entries(cur.propertyDeltas)) {
|
|
||||||
const plugin = this.config[propertyId];
|
|
||||||
if (!plugin) continue;
|
|
||||||
|
|
||||||
const propertyState = acc[cur.id].properties[propertyId];
|
const propertyState = acc[cur.id].properties[propertyId];
|
||||||
const context = { entityState, resolvedValues };
|
|
||||||
|
|
||||||
// Process all deltas for this property
|
// Process all deltas for this property
|
||||||
for (const delta of deltas || []) {
|
for (const delta of deltas || []) {
|
||||||
const value = extractValueFromDelta(propertyId, delta);
|
const value = extractValueFromDelta(propertyId, delta);
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
propertyState.state = plugin.update(
|
propertyState.state = propertyState.plugin.update(propertyState.state, value, delta);
|
||||||
propertyState.state,
|
|
||||||
value,
|
|
||||||
delta,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the resolved value after each update
|
|
||||||
try {
|
|
||||||
const resolved = plugin.resolve(propertyState.state, context);
|
|
||||||
if (resolved !== undefined) {
|
|
||||||
resolvedValues[propertyId] = resolved;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore resolution errors during reduction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,39 +107,11 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
|||||||
for (const [entityId, entity] of Object.entries(cur)) {
|
for (const [entityId, entity] of Object.entries(cur)) {
|
||||||
const entityResult: { id: string; properties: EntityProperties } = { id: entityId, properties: {} };
|
const entityResult: { id: string; properties: EntityProperties } = { id: entityId, properties: {} };
|
||||||
|
|
||||||
// First pass: collect all property states
|
|
||||||
const entityState: Record<string, unknown> = {};
|
|
||||||
const resolvedValues: Record<string, PropertyTypes> = {};
|
|
||||||
|
|
||||||
// Initialize with current states and resolve all properties
|
|
||||||
for (const [propertyId, propertyState] of Object.entries(entity.properties)) {
|
for (const [propertyId, propertyState] of Object.entries(entity.properties)) {
|
||||||
entityState[propertyId] = propertyState.state;
|
const resolvedValue = propertyState.plugin.resolve(propertyState.state);
|
||||||
// Initial resolution with empty context
|
// Only add the property if the resolved value is not undefined
|
||||||
try {
|
if (resolvedValue !== undefined) {
|
||||||
const resolved = propertyState.plugin.resolve(propertyState.state, {
|
entityResult.properties[propertyId] = resolvedValue;
|
||||||
entityState: {},
|
|
||||||
resolvedValues: {}
|
|
||||||
});
|
|
||||||
if (resolved !== undefined) {
|
|
||||||
resolvedValues[propertyId] = resolved;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore resolution errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: resolve with full context
|
|
||||||
for (const [propertyId, propertyState] of Object.entries(entity.properties)) {
|
|
||||||
const context = { entityState, resolvedValues };
|
|
||||||
try {
|
|
||||||
const resolvedValue = propertyState.plugin.resolve(propertyState.state, context);
|
|
||||||
if (resolvedValue !== undefined) {
|
|
||||||
entityResult.properties[propertyId] = resolvedValue;
|
|
||||||
// Update the resolved value for dependent properties
|
|
||||||
resolvedValues[propertyId] = resolvedValue;
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore resolution errors
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,12 +137,7 @@ export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTyp
|
|||||||
return { timestamp: 0 };
|
return { timestamp: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||||
currentState: { value?: PropertyTypes, timestamp: number },
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
if (delta.timeCreated > currentState.timestamp) {
|
if (delta.timeCreated > currentState.timestamp) {
|
||||||
return {
|
return {
|
||||||
value: newValue,
|
value: newValue,
|
||||||
@ -254,12 +160,7 @@ export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTy
|
|||||||
return { timestamp: Infinity };
|
return { timestamp: Infinity };
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { value?: PropertyTypes, timestamp: number }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||||
currentState: { value?: PropertyTypes, timestamp: number },
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
if (delta.timeCreated < currentState.timestamp) {
|
if (delta.timeCreated < currentState.timestamp) {
|
||||||
return {
|
return {
|
||||||
value: newValue,
|
value: newValue,
|
||||||
@ -284,12 +185,7 @@ export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: st
|
|||||||
return { values: [] };
|
return { values: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { values: { value: string, timestamp: number }[] }, newValue: PropertyTypes, delta: CollapsedDelta) {
|
||||||
currentState: { values: { value: string, timestamp: number }[] },
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
if (typeof newValue === 'string') {
|
if (typeof newValue === 'string') {
|
||||||
// Check if this value already exists (avoid duplicates)
|
// Check if this value already exists (avoid duplicates)
|
||||||
const exists = currentState.values.some(v => v.value === newValue);
|
const exists = currentState.values.some(v => v.value === newValue);
|
||||||
@ -305,10 +201,7 @@ export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: st
|
|||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(state: { values: { value: string, timestamp: number }[] }): PropertyTypes {
|
||||||
state: { values: { value: string, timestamp: number }[] },
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
): PropertyTypes {
|
|
||||||
return state.values.map(v => v.value).join(this.separator);
|
return state.values.map(v => v.value).join(this.separator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,32 +214,24 @@ export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyT
|
|||||||
return { votes: new Map() };
|
return { votes: new Map() };
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { votes: Map<PropertyTypes, number> }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||||
currentState: { votes: Map<PropertyTypes, number> },
|
const currentCount = currentState.votes.get(newValue) || 0;
|
||||||
newValue: PropertyTypes,
|
currentState.votes.set(newValue, currentCount + 1);
|
||||||
_delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
const count = (currentState.votes.get(newValue) || 0) + 1;
|
|
||||||
currentState.votes.set(newValue, count);
|
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(state: { votes: Map<PropertyTypes, number> }): PropertyTypes {
|
||||||
state: { votes: Map<PropertyTypes, number> },
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
): PropertyTypes {
|
|
||||||
let maxVotes = 0;
|
let maxVotes = 0;
|
||||||
let result: PropertyTypes = '';
|
let winner: PropertyTypes = '';
|
||||||
|
|
||||||
for (const [value, count] of state.votes.entries()) {
|
for (const [value, votes] of state.votes.entries()) {
|
||||||
if (count > maxVotes) {
|
if (votes > maxVotes) {
|
||||||
maxVotes = count;
|
maxVotes = votes;
|
||||||
result = value;
|
winner = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return winner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,12 +243,7 @@ export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { min?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||||
currentState: { min?: number },
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
_delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
if (typeof newValue === 'number') {
|
if (typeof newValue === 'number') {
|
||||||
if (currentState.min === undefined || newValue < currentState.min) {
|
if (currentState.min === undefined || newValue < currentState.min) {
|
||||||
return { min: newValue };
|
return { min: newValue };
|
||||||
@ -372,10 +252,7 @@ export class MinPlugin implements ResolverPlugin<{ min?: number }> {
|
|||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(state: { min?: number }): PropertyTypes | undefined {
|
||||||
state: { min?: number },
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
): PropertyTypes | undefined {
|
|
||||||
return state.min;
|
return state.min;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -387,12 +264,7 @@ export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(currentState: { max?: number }, newValue: PropertyTypes, _delta: CollapsedDelta) {
|
||||||
currentState: { max?: number },
|
|
||||||
newValue: PropertyTypes,
|
|
||||||
_delta: CollapsedDelta,
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
) {
|
|
||||||
if (typeof newValue === 'number') {
|
if (typeof newValue === 'number') {
|
||||||
if (currentState.max === undefined || newValue > currentState.max) {
|
if (currentState.max === undefined || newValue > currentState.max) {
|
||||||
return { max: newValue };
|
return { max: newValue };
|
||||||
@ -401,10 +273,7 @@ export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
|
|||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(state: { max?: number }): PropertyTypes | undefined {
|
||||||
state: { max?: number },
|
|
||||||
_context?: { entityState: Record<string, unknown>, resolvedValues: Record<string, PropertyTypes> }
|
|
||||||
): PropertyTypes | undefined {
|
|
||||||
return state.max;
|
return state.max;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user