Separated custom-resolver.ts into separate files

This commit is contained in:
Lentil Hoffman 2025-06-23 00:16:26 -05:00
parent cf29338c9b
commit f6790bf90d
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
12 changed files with 336 additions and 263 deletions

View File

@ -1,6 +1,6 @@
import { RhizomeNode, Lossless, createDelta } from "../../../../src";
import { CollapsedDelta } from "../../../../src/views/lossless";
import { import {
RhizomeNode,
Lossless,
CustomResolver, CustomResolver,
ResolverPlugin, ResolverPlugin,
LastWriteWinsPlugin, LastWriteWinsPlugin,
@ -8,11 +8,10 @@ import {
ConcatenationPlugin, ConcatenationPlugin,
MajorityVotePlugin, MajorityVotePlugin,
MinPlugin, MinPlugin,
MaxPlugin, MaxPlugin
PropertyTypes, } from "../../../../src/views/resolvers/custom-resolvers";
CollapsedDelta,
createDelta type PropertyTypes = string | number | boolean | null;
} from "../../../../src";
describe('Custom Resolvers', () => { describe('Custom Resolvers', () => {
let node: RhizomeNode; let node: RhizomeNode;

View File

@ -5,3 +5,4 @@
- [ ] Rename/consolidate, lossless view() and compose() --> composeView() - [ ] Rename/consolidate, lossless view() and compose() --> composeView()
- [ ] Rename Lossless to HyperView - [ ] Rename Lossless to HyperView
- [ ] Rename Lossy to View - [ ] Rename Lossy to View
- [ ] Consider whether we should use collapsed deltas

View File

@ -0,0 +1,3 @@
export * from './plugin';
export * from './resolver';
export * from './plugins';

View File

@ -0,0 +1,45 @@
import { PropertyID, PropertyTypes } from "../../../core/types";
import { CollapsedDelta } from "../../lossless";
/**
* Plugin interface for custom resolvers
*/
export interface ResolverPlugin<T = unknown> {
name: string;
/**
* Array of property IDs that this plugin depends on.
* These properties will be processed before this plugin.
*/
dependencies?: PropertyID[];
/**
* Initialize the state for a property
*/
initialize(): T;
/**
* Process a new value for the property
*/
update(
currentState: T,
newValue: PropertyTypes,
delta: CollapsedDelta,
allStates?: Record<PropertyID, unknown>
): T;
/**
* Resolve the final value from the accumulated state
*/
resolve(
state: T,
allStates?: Record<PropertyID, unknown>
): PropertyTypes | undefined;
}
/**
* Configuration for custom resolver
*/
export type CustomResolverConfig = {
[propertyId: PropertyID]: ResolverPlugin;
};

View File

@ -0,0 +1,57 @@
import { PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* Concatenation plugin (for string values)
*
* Concatenates all string values with a separator
*/
export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: string, timestamp: number }[] }> {
name = 'concatenation';
dependencies: string[] = [];
constructor(private separator: string = ' ') {}
initialize() {
return { values: [] };
}
update(
currentState: { values: { value: string, timestamp: number }[] },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
if (typeof newValue === 'string') {
return {
values: [
...currentState.values,
{ value: newValue, timestamp: delta.timeCreated }
]
};
}
return currentState;
}
resolve(
state: { values: { value: string, timestamp: number }[] },
_allStates?: Record<string, unknown>
): PropertyTypes {
// Sort by timestamp to ensure consistent ordering
const sortedValues = [...state.values].sort((a, b) => a.timestamp - b.timestamp);
// Use a Set to track seen values and keep only the first occurrence of each value
const seen = new Set<string>();
const uniqueValues: string[] = [];
for (const { value } of sortedValues) {
if (!seen.has(value)) {
seen.add(value);
uniqueValues.push(value);
}
}
return uniqueValues.join(this.separator);
}
}

View File

@ -0,0 +1,40 @@
import { PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* First Write Wins plugin
*
* Keeps the first value that was written, ignoring subsequent writes
*/
export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
name = 'first-write-wins';
dependencies: string[] = [];
initialize() {
return { timestamp: Infinity };
}
update(
currentState: { value?: PropertyTypes, timestamp: number },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
// Only update if this delta is earlier than our current earliest
if (delta.timeCreated < currentState.timestamp) {
return {
value: newValue,
timestamp: delta.timeCreated
};
}
return currentState;
}
resolve(
state: { value?: PropertyTypes, timestamp: number },
_allStates?: Record<string, unknown>
): PropertyTypes | undefined {
return state.value;
}
}

View File

@ -0,0 +1,6 @@
export * from './last-write-wins.plugin';
export * from './first-write-wins.plugin';
export * from './concatenation.plugin';
export * from './majority-vote.plugin';
export * from './min.plugin';
export * from './max.plugin';

View File

@ -0,0 +1,39 @@
import { PropertyID, PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* Last Write Wins plugin
*
* Keeps the most recent value based on the delta's timestamp
*/
export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
name = 'last-write-wins';
dependencies: PropertyID[] = [];
initialize() {
return { timestamp: 0 };
}
update(
currentState: { value?: PropertyTypes, timestamp: number },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
if (delta.timeCreated > currentState.timestamp) {
return {
value: newValue,
timestamp: delta.timeCreated
};
}
return currentState;
}
resolve(
state: { value?: PropertyTypes, timestamp: number },
_allStates?: Record<string, unknown>
): PropertyTypes {
return state.value || '';
}
}

View File

@ -0,0 +1,45 @@
import { PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* Majority vote plugin
*
* Returns the value that appears most frequently
*/
export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyTypes, number> }> {
name = 'majority-vote';
dependencies: string[] = [];
initialize() {
return { votes: new Map<PropertyTypes, number>() };
}
update(
currentState: { votes: Map<PropertyTypes, number> },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
const currentCount = currentState.votes.get(newValue) || 0;
currentState.votes.set(newValue, currentCount + 1);
return currentState;
}
resolve(
state: { votes: Map<PropertyTypes, number> },
_allStates?: Record<string, unknown>
): PropertyTypes | undefined {
let maxCount = 0;
let result: PropertyTypes | undefined;
state.votes.forEach((count, value) => {
if (count > maxCount) {
maxCount = count;
result = value;
}
});
return result;
}
}

View File

@ -0,0 +1,38 @@
import { PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* Numeric max plugin
*
* Tracks the maximum numeric value
*/
export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
name = 'max';
dependencies: string[] = [];
initialize() {
return { max: undefined };
}
update(
currentState: { max?: number },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
const numValue = typeof newValue === 'number' ? newValue : parseFloat(String(newValue));
if (!isNaN(numValue) && (currentState.max === undefined || numValue > currentState.max)) {
return { max: numValue };
}
return currentState;
}
resolve(
state: { max?: number },
_allStates?: Record<string, unknown>
): PropertyTypes | undefined {
return state.max;
}
}

View File

@ -0,0 +1,38 @@
import { PropertyTypes } from "../../../../core/types";
import { CollapsedDelta } from "../../../lossless";
import { ResolverPlugin } from "../plugin";
/**
* Numeric min plugin
*
* Tracks the minimum numeric value
*/
export class MinPlugin implements ResolverPlugin<{ min?: number }> {
name = 'min';
dependencies: string[] = [];
initialize() {
return { min: undefined };
}
update(
currentState: { min?: number },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<string, unknown>
) {
const numValue = typeof newValue === 'number' ? newValue : parseFloat(String(newValue));
if (!isNaN(numValue) && (currentState.min === undefined || numValue < currentState.min)) {
return { min: numValue };
}
return currentState;
}
resolve(
state: { min?: number },
_allStates?: Record<string, unknown>
): PropertyTypes | undefined {
return state.min;
}
}

View File

@ -1,40 +1,8 @@
import { EntityProperties } from "../../core/entity"; import { EntityProperties } from "../../../core/entity";
import { CollapsedDelta, Lossless, LosslessViewOne } from "../lossless"; import { CollapsedDelta, Lossless, LosslessViewOne } from "../../lossless";
import { Lossy } from '../lossy'; import { Lossy } from '../../lossy';
import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../core/types"; import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../../core/types";
import { ResolverPlugin } from "./plugin";
// Plugin interface for custom resolvers
export interface ResolverPlugin<T = unknown> {
name: string;
/**
* Array of property IDs that this plugin depends on.
* These properties will be processed before this plugin.
*/
dependencies?: PropertyID[];
// Initialize the state for a property
initialize(): T;
// Process a new value for the property
update(
currentState: T,
newValue: PropertyTypes,
delta: CollapsedDelta,
allStates?: Record<PropertyID, unknown>
): T;
// Resolve the final value from the accumulated state
resolve(
state: T,
allStates?: Record<PropertyID, unknown>
): PropertyTypes | undefined;
}
// Configuration for custom resolver
export type CustomResolverConfig = {
[propertyId: PropertyID]: ResolverPlugin;
};
type PropertyState = { type PropertyState = {
plugin: ResolverPlugin; plugin: ResolverPlugin;
@ -57,7 +25,9 @@ type CustomResolverResult = ViewMany<{
properties: EntityProperties; properties: EntityProperties;
}>; }>;
// Extract value from delta for a specific property /**
* Extract value from delta for a specific property
*/
function extractValueFromDelta(propertyId: PropertyID, delta: CollapsedDelta): PropertyTypes | undefined { function extractValueFromDelta(propertyId: PropertyID, delta: CollapsedDelta): PropertyTypes | undefined {
for (const pointer of delta.pointers) { for (const pointer of delta.pointers) {
for (const [key, value] of Object.entries(pointer)) { for (const [key, value] of Object.entries(pointer)) {
@ -71,11 +41,11 @@ function extractValueFromDelta(propertyId: PropertyID, delta: CollapsedDelta): P
export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResolverResult> { export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResolverResult> {
private executionOrder: PropertyID[]; private executionOrder: PropertyID[];
private readonly config: CustomResolverConfig; private readonly config: Record<PropertyID, ResolverPlugin>;
constructor( constructor(
lossless: Lossless, lossless: Lossless,
config: CustomResolverConfig config: Record<PropertyID, ResolverPlugin>
) { ) {
super(lossless); super(lossless);
this.config = config; this.config = config;
@ -258,212 +228,4 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
return res; return res;
} }
}
// Built-in plugin implementations
// Last Write Wins plugin
export class LastWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
name = 'last-write-wins';
dependencies: PropertyID[] = [];
initialize() {
return { timestamp: 0 };
}
update(
currentState: { value?: PropertyTypes, timestamp: number },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
if (delta.timeCreated > currentState.timestamp) {
return {
value: newValue,
timestamp: delta.timeCreated
};
}
return currentState;
}
resolve(
state: { value?: PropertyTypes, timestamp: number },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes {
return state.value || '';
}
}
// First Write Wins plugin
export class FirstWriteWinsPlugin implements ResolverPlugin<{ value?: PropertyTypes, timestamp: number }> {
name = 'first-write-wins';
dependencies: PropertyID[] = [];
initialize() {
return { timestamp: Infinity };
}
update(
currentState: { value?: PropertyTypes, timestamp: number },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
if (delta.timeCreated < currentState.timestamp) {
return {
value: newValue,
timestamp: delta.timeCreated
};
}
return currentState;
}
resolve(
state: { value?: PropertyTypes, timestamp: number },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes {
return state.value || '';
}
}
// Concatenation plugin (for string values)
export class ConcatenationPlugin implements ResolverPlugin<{ values: { value: string, timestamp: number }[] }> {
name = 'concatenation';
dependencies: PropertyID[] = [];
constructor(private separator: string = ' ') { }
initialize() {
return { values: [] };
}
update(
currentState: { values: { value: string, timestamp: number }[] },
newValue: PropertyTypes,
delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
if (typeof newValue === 'string') {
// Check if this value already exists (avoid duplicates)
const exists = currentState.values.some(v => v.value === newValue);
if (!exists) {
currentState.values.push({
value: newValue,
timestamp: delta.timeCreated
});
// Sort by timestamp to maintain chronological order
currentState.values.sort((a, b) => a.timestamp - b.timestamp);
}
}
return currentState;
}
resolve(
state: { values: { value: string, timestamp: number }[] },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes {
return state.values.map(v => v.value).join(this.separator);
}
}
// Majority vote plugin
export class MajorityVotePlugin implements ResolverPlugin<{ votes: Map<PropertyTypes, number> }> {
name = 'majority-vote';
dependencies: PropertyID[] = [];
initialize() {
return { votes: new Map() };
}
update(
currentState: { votes: Map<PropertyTypes, number> },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
const currentCount = currentState.votes.get(newValue) || 0;
currentState.votes.set(newValue, currentCount + 1);
return currentState;
}
resolve(
state: { votes: Map<PropertyTypes, number> },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes {
let maxVotes = 0;
let winner: PropertyTypes = '';
for (const [value, votes] of state.votes.entries()) {
if (votes > maxVotes) {
maxVotes = votes;
winner = value;
}
}
return winner;
}
}
// Numeric min plugin
export class MinPlugin implements ResolverPlugin<{ min?: number }> {
name = 'min';
dependencies: PropertyID[] = [];
initialize() {
return {};
}
update(
currentState: { min?: number },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
if (typeof newValue === 'number') {
if (currentState.min === undefined || newValue < currentState.min) {
return { min: newValue };
}
}
return currentState;
}
resolve(
state: { min?: number },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes | undefined {
return state.min;
}
}
// Numeric max plugin
export class MaxPlugin implements ResolverPlugin<{ max?: number }> {
name = 'max';
dependencies: PropertyID[] = [];
initialize() {
return {};
}
update(
currentState: { max?: number },
newValue: PropertyTypes,
_delta: CollapsedDelta,
_allStates?: Record<PropertyID, unknown>
) {
if (typeof newValue === 'number') {
if (currentState.max === undefined || newValue > currentState.max) {
return { max: newValue };
}
}
return currentState;
}
resolve(
state: { max?: number },
_allStates?: Record<PropertyID, unknown>
): PropertyTypes | undefined {
return state.max;
}
} }