Separated custom-resolver.ts into separate files
This commit is contained in:
parent
cf29338c9b
commit
f6790bf90d
@ -1,18 +1,17 @@
|
||||
import {
|
||||
RhizomeNode,
|
||||
Lossless,
|
||||
CustomResolver,
|
||||
ResolverPlugin,
|
||||
LastWriteWinsPlugin,
|
||||
FirstWriteWinsPlugin,
|
||||
ConcatenationPlugin,
|
||||
MajorityVotePlugin,
|
||||
MinPlugin,
|
||||
MaxPlugin,
|
||||
PropertyTypes,
|
||||
CollapsedDelta,
|
||||
createDelta
|
||||
} from "../../../../src";
|
||||
import { RhizomeNode, Lossless, createDelta } from "../../../../src";
|
||||
import { CollapsedDelta } from "../../../../src/views/lossless";
|
||||
import {
|
||||
CustomResolver,
|
||||
ResolverPlugin,
|
||||
LastWriteWinsPlugin,
|
||||
FirstWriteWinsPlugin,
|
||||
ConcatenationPlugin,
|
||||
MajorityVotePlugin,
|
||||
MinPlugin,
|
||||
MaxPlugin
|
||||
} from "../../../../src/views/resolvers/custom-resolvers";
|
||||
|
||||
type PropertyTypes = string | number | boolean | null;
|
||||
|
||||
describe('Custom Resolvers', () => {
|
||||
let node: RhizomeNode;
|
||||
|
@ -5,3 +5,4 @@
|
||||
- [ ] Rename/consolidate, lossless view() and compose() --> composeView()
|
||||
- [ ] Rename Lossless to HyperView
|
||||
- [ ] Rename Lossy to View
|
||||
- [ ] Consider whether we should use collapsed deltas
|
||||
|
3
src/views/resolvers/custom-resolvers/index.ts
Normal file
3
src/views/resolvers/custom-resolvers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './plugin';
|
||||
export * from './resolver';
|
||||
export * from './plugins';
|
45
src/views/resolvers/custom-resolvers/plugin.ts
Normal file
45
src/views/resolvers/custom-resolvers/plugin.ts
Normal 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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
6
src/views/resolvers/custom-resolvers/plugins/index.ts
Normal file
6
src/views/resolvers/custom-resolvers/plugins/index.ts
Normal 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';
|
@ -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 || '';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
38
src/views/resolvers/custom-resolvers/plugins/max.plugin.ts
Normal file
38
src/views/resolvers/custom-resolvers/plugins/max.plugin.ts
Normal 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;
|
||||
}
|
||||
}
|
38
src/views/resolvers/custom-resolvers/plugins/min.plugin.ts
Normal file
38
src/views/resolvers/custom-resolvers/plugins/min.plugin.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -1,40 +1,8 @@
|
||||
import { EntityProperties } from "../../core/entity";
|
||||
import { CollapsedDelta, Lossless, LosslessViewOne } from "../lossless";
|
||||
import { Lossy } from '../lossy';
|
||||
import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../core/types";
|
||||
|
||||
// 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;
|
||||
};
|
||||
import { EntityProperties } from "../../../core/entity";
|
||||
import { CollapsedDelta, Lossless, LosslessViewOne } from "../../lossless";
|
||||
import { Lossy } from '../../lossy';
|
||||
import { DomainEntityID, PropertyID, PropertyTypes, ViewMany } from "../../../core/types";
|
||||
import { ResolverPlugin } from "./plugin";
|
||||
|
||||
type PropertyState = {
|
||||
plugin: ResolverPlugin;
|
||||
@ -57,7 +25,9 @@ type CustomResolverResult = ViewMany<{
|
||||
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 {
|
||||
for (const pointer of delta.pointers) {
|
||||
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> {
|
||||
private executionOrder: PropertyID[];
|
||||
private readonly config: CustomResolverConfig;
|
||||
private readonly config: Record<PropertyID, ResolverPlugin>;
|
||||
|
||||
constructor(
|
||||
lossless: Lossless,
|
||||
config: CustomResolverConfig
|
||||
config: Record<PropertyID, ResolverPlugin>
|
||||
) {
|
||||
super(lossless);
|
||||
this.config = config;
|
||||
@ -258,212 +228,4 @@ export class CustomResolver extends Lossy<CustomResolverAccumulator, CustomResol
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user