progress
This commit is contained in:
parent
9957dccddd
commit
880affad1c
@ -16,22 +16,7 @@ describe('Edge Cases', () => {
|
|||||||
lossless = new Lossless(node);
|
lossless = new Lossless(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle null and undefined values', () => {
|
test('should handle null values', () => {
|
||||||
lossless.ingestDelta(
|
|
||||||
createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(1000)
|
|
||||||
.setProperty('test1', 'value', null, 'test')
|
|
||||||
.buildV1()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use null instead of undefined as it's a valid PropertyType
|
|
||||||
lossless.ingestDelta(
|
|
||||||
createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(2000)
|
|
||||||
.setProperty('test1', 'value', null, 'test')
|
|
||||||
.buildV1()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a type-safe plugin that handles null/undefined values
|
// Create a type-safe plugin that handles null/undefined values
|
||||||
class NullSafeLastWriteWinsPlugin implements ResolverPlugin<{ value: PropertyTypes | null, timestamp: number }, never> {
|
class NullSafeLastWriteWinsPlugin implements ResolverPlugin<{ value: PropertyTypes | null, timestamp: number }, never> {
|
||||||
readonly dependencies = [] as const;
|
readonly dependencies = [] as const;
|
||||||
@ -42,10 +27,11 @@ describe('Edge Cases', () => {
|
|||||||
|
|
||||||
update(
|
update(
|
||||||
currentState: { value: PropertyTypes | null, timestamp: number },
|
currentState: { value: PropertyTypes | null, timestamp: number },
|
||||||
newValue: PropertyTypes,
|
newValue?: PropertyTypes,
|
||||||
delta: CollapsedDelta,
|
delta?: CollapsedDelta,
|
||||||
_dependencies: DependencyStates
|
|
||||||
) {
|
) {
|
||||||
|
if (newValue === undefined) return currentState;
|
||||||
|
if (!delta) return currentState;
|
||||||
if (delta.timeCreated > currentState.timestamp) {
|
if (delta.timeCreated > currentState.timestamp) {
|
||||||
return { value: newValue, timestamp: delta.timeCreated };
|
return { value: newValue, timestamp: delta.timeCreated };
|
||||||
}
|
}
|
||||||
@ -54,9 +40,8 @@ describe('Edge Cases', () => {
|
|||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
state: { value: PropertyTypes | null, timestamp: number },
|
state: { value: PropertyTypes | null, timestamp: number },
|
||||||
_dependencies: DependencyStates
|
|
||||||
): PropertyTypes | undefined {
|
): PropertyTypes | undefined {
|
||||||
return state.value ?? undefined;
|
return state.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,29 +49,20 @@ describe('Edge Cases', () => {
|
|||||||
value: new NullSafeLastWriteWinsPlugin()
|
value: new NullSafeLastWriteWinsPlugin()
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = resolver.resolve() || [];
|
|
||||||
expect(Array.isArray(results)).toBe(true);
|
|
||||||
const test1 = results.find(r => r.id === 'test1');
|
|
||||||
expect(test1).toBeDefined();
|
|
||||||
expect(test1?.properties.value).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle concurrent updates with same timestamp', () => {
|
|
||||||
// Two updates with the same timestamp
|
|
||||||
lossless.ingestDelta(
|
lossless.ingestDelta(
|
||||||
createDelta('user1', 'host1')
|
createDelta('user1', 'host1')
|
||||||
.withTimestamp(1000)
|
.withTimestamp(1000)
|
||||||
.setProperty('test2', 'value', 'first', 'test')
|
.setProperty('test2', 'value', null, 'test')
|
||||||
.buildV1()
|
.buildV1()
|
||||||
);
|
);
|
||||||
|
|
||||||
lossless.ingestDelta(
|
const results = resolver.resolve() || {};
|
||||||
createDelta('user2', 'host2')
|
const test1 = results['test2']
|
||||||
.withTimestamp(1000) // Same timestamp
|
expect(test1).toBeDefined();
|
||||||
.setProperty('test2', 'value', 'second', 'test')
|
expect(test1?.properties.value).toBeNull();
|
||||||
.buildV1()
|
});
|
||||||
);
|
|
||||||
|
|
||||||
|
test('should handle concurrent updates with same timestamp', () => {
|
||||||
// Custom plugin that handles concurrent updates with the same timestamp
|
// Custom plugin that handles concurrent updates with the same timestamp
|
||||||
class ConcurrentUpdatePlugin implements ResolverPlugin<{ value: PropertyTypes, timestamp: number }, never> {
|
class ConcurrentUpdatePlugin implements ResolverPlugin<{ value: PropertyTypes, timestamp: number }, never> {
|
||||||
readonly dependencies = [] as const;
|
readonly dependencies = [] as const;
|
||||||
@ -123,25 +99,31 @@ describe('Edge Cases', () => {
|
|||||||
value: new ConcurrentUpdatePlugin()
|
value: new ConcurrentUpdatePlugin()
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = resolver.resolve() || [];
|
// Two updates with the same timestamp
|
||||||
expect(Array.isArray(results)).toBe(true);
|
lossless.ingestDelta(
|
||||||
const test2 = results.find(r => r.id === 'test2');
|
createDelta('user1', 'host1')
|
||||||
|
.withTimestamp(1000)
|
||||||
|
.setProperty('test2', 'value', null, 'test')
|
||||||
|
.buildV1()
|
||||||
|
);
|
||||||
|
|
||||||
|
lossless.ingestDelta(
|
||||||
|
createDelta('user2', 'host2')
|
||||||
|
.withTimestamp(1000) // Same timestamp
|
||||||
|
.setProperty('test2', 'value', 'xylophone', 'test')
|
||||||
|
.buildV1()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const results = resolver.resolve() || {};
|
||||||
|
const test2 = results['test2'];
|
||||||
expect(test2).toBeDefined();
|
expect(test2).toBeDefined();
|
||||||
// Should pick one of the values deterministically
|
// Should pick one of the values deterministically
|
||||||
expect(test2?.properties.value).toBe('first');
|
expect(test2?.properties.value).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle very large numbers of updates', () => {
|
test('should handle very large numbers of updates', () => {
|
||||||
// Add 1000 updates
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
lossless.ingestDelta(
|
|
||||||
createDelta('user1', 'host1')
|
|
||||||
.withTimestamp(1000 + i)
|
|
||||||
.setProperty('test3', 'counter', i, 'test')
|
|
||||||
.buildV1()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin that handles large numbers of updates efficiently
|
// Plugin that handles large numbers of updates efficiently
|
||||||
class CounterPlugin implements ResolverPlugin<{ count: number }, never> {
|
class CounterPlugin implements ResolverPlugin<{ count: number }, never> {
|
||||||
readonly dependencies = [] as const;
|
readonly dependencies = [] as const;
|
||||||
@ -171,9 +153,18 @@ describe('Edge Cases', () => {
|
|||||||
counter: new CounterPlugin()
|
counter: new CounterPlugin()
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = resolver.resolve() || [];
|
// Add 1000 updates
|
||||||
expect(Array.isArray(results)).toBe(true);
|
for (let i = 0; i < 1000; i++) {
|
||||||
const test3 = results.find(r => r.id === 'test3');
|
lossless.ingestDelta(
|
||||||
|
createDelta('user1', 'host1')
|
||||||
|
.withTimestamp(1000 + i)
|
||||||
|
.setProperty('test3', 'counter', i, 'test')
|
||||||
|
.buildV1()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = resolver.resolve() || {};
|
||||||
|
const test3 = results['test3']
|
||||||
expect(test3).toBeDefined();
|
expect(test3).toBeDefined();
|
||||||
// Should handle large numbers of updates efficiently
|
// Should handle large numbers of updates efficiently
|
||||||
expect(test3?.properties.counter).toBe(1000); // Should count all 1000 updates
|
expect(test3?.properties.counter).toBe(1000); // Should count all 1000 updates
|
||||||
@ -183,28 +174,22 @@ describe('Edge Cases', () => {
|
|||||||
// No deltas added - should handle empty state
|
// No deltas added - should handle empty state
|
||||||
// Plugin that handles missing properties gracefully
|
// Plugin that handles missing properties gracefully
|
||||||
class MissingPropertyPlugin implements ResolverPlugin<{ initialized: boolean }, never> {
|
class MissingPropertyPlugin implements ResolverPlugin<{ initialized: boolean }, never> {
|
||||||
private _initialized = false;
|
|
||||||
readonly dependencies = [] as const;
|
readonly dependencies = [] as const;
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this._initialized = true;
|
|
||||||
return { initialized: true };
|
return { initialized: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(
|
||||||
currentState: { initialized: boolean },
|
currentState: { initialized: boolean },
|
||||||
_newValue: PropertyTypes,
|
|
||||||
_delta: CollapsedDelta,
|
|
||||||
_dependencies: DependencyStates
|
|
||||||
) {
|
) {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
_state: { initialized: boolean },
|
state: { initialized: boolean }
|
||||||
_dependencies: DependencyStates
|
|
||||||
): boolean {
|
): boolean {
|
||||||
return this._initialized;
|
return state.initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ export abstract class Lossy<Accumulator, Result = Accumulator> {
|
|||||||
// Resolve the current state of the view
|
// Resolve the current state of the view
|
||||||
resolve(entityIds?: DomainEntityID[]): Result | undefined {
|
resolve(entityIds?: DomainEntityID[]): Result | undefined {
|
||||||
if (!this.accumulator) {
|
if (!this.accumulator) {
|
||||||
return undefined;
|
this.accumulator =this.initializer?.() || {} as Accumulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entityIds) {
|
if (!entityIds) {
|
||||||
|
@ -5,8 +5,8 @@ import { ResolverPlugin, DependencyStates } from "./plugin";
|
|||||||
import { EntityRecord } from "@src/core/entity";
|
import { EntityRecord } from "@src/core/entity";
|
||||||
import Debug from 'debug';
|
import Debug from 'debug';
|
||||||
|
|
||||||
const debug = Debug('rz:resolver');
|
const debug = Debug('rz:custom-resolver');
|
||||||
const debugState = Debug('rz:resolver:state');
|
const debugState = Debug('rz:custom-resolver:state');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state of a property for a single entity
|
* The state of a property for a single entity
|
||||||
@ -224,6 +224,28 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
return dependencyStates;
|
return dependencyStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializePlugins(acc: Accumulator, entityId: DomainEntityID) {
|
||||||
|
if (!acc[entityId]) {
|
||||||
|
acc[entityId] = {};
|
||||||
|
}
|
||||||
|
const entityState = acc[entityId];
|
||||||
|
|
||||||
|
for (const pluginId of this.executionOrder) {
|
||||||
|
const pluginKey = this.pluginKeyFromBasename(pluginId);
|
||||||
|
const plugin = this.config[pluginKey];
|
||||||
|
if (!plugin) throw new Error(`Plugin for property ${pluginId} not found`);
|
||||||
|
|
||||||
|
// We need to resolve dependencies, including entity properties that are not plugins.
|
||||||
|
const dependencies = this.getDependencyStates(entityState, plugin);
|
||||||
|
debug('Dependencies for', pluginId, ':', JSON.stringify(dependencies));
|
||||||
|
|
||||||
|
// Initialize the plugin if it hasn't been initialized yet
|
||||||
|
entityState[pluginKey] = entityState[pluginKey] ?? plugin.initialize(dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityState };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the state with new deltas from the view
|
* Update the state with new deltas from the view
|
||||||
*/
|
*/
|
||||||
@ -231,10 +253,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
debug(`Processing deltas for entity: ${entityId}`);
|
debug(`Processing deltas for entity: ${entityId}`);
|
||||||
debug('Property deltas:', JSON.stringify(propertyDeltas));
|
debug('Property deltas:', JSON.stringify(propertyDeltas));
|
||||||
|
|
||||||
if (!acc[entityId]) {
|
const { entityState } = this.initializePlugins(acc, entityId);
|
||||||
acc[entityId] = {};
|
|
||||||
}
|
|
||||||
const entityState = acc[entityId];
|
|
||||||
|
|
||||||
// Now let's go through each plugin in order.
|
// Now let's go through each plugin in order.
|
||||||
for (const pluginId of this.executionOrder) {
|
for (const pluginId of this.executionOrder) {
|
||||||
@ -244,12 +263,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
|
|
||||||
debug(`Processing plugin: ${pluginId} (key: ${pluginKey})`);
|
debug(`Processing plugin: ${pluginId} (key: ${pluginKey})`);
|
||||||
|
|
||||||
// We need to resolve dependencies, including entity properties that are not plugins.
|
const pluginState = entityState[pluginKey];
|
||||||
const dependencies = this.getDependencyStates(entityState, plugin);
|
|
||||||
debug('Dependencies for', pluginId, ':', JSON.stringify(dependencies));
|
|
||||||
|
|
||||||
// Initialize the plugin if it hasn't been initialized yet
|
|
||||||
const pluginState = entityState[pluginKey] ?? plugin.initialize(dependencies);
|
|
||||||
|
|
||||||
const deltaPropertyValues : Record<PropertyID, PropertyTypes> = {};
|
const deltaPropertyValues : Record<PropertyID, PropertyTypes> = {};
|
||||||
let propertyValue : PropertyTypes | undefined;
|
let propertyValue : PropertyTypes | undefined;
|
||||||
@ -276,6 +290,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the plugin state with the new delta
|
// Update the plugin state with the new delta
|
||||||
|
const dependencies = this.getDependencyStates(entityState, plugin);
|
||||||
entityState[pluginKey] = plugin.update(pluginState, propertyValue, updateDelta, dependencies);
|
entityState[pluginKey] = plugin.update(pluginState, propertyValue, updateDelta, dependencies);
|
||||||
debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey]));
|
debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey]));
|
||||||
}
|
}
|
||||||
@ -289,6 +304,9 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
|
|
||||||
for (const entityId in acc) {
|
for (const entityId in acc) {
|
||||||
if (!entityIds.includes(entityId)) continue;
|
if (!entityIds.includes(entityId)) continue;
|
||||||
|
|
||||||
|
this.initializePlugins(acc, entityId);
|
||||||
|
|
||||||
result[entityId] = {
|
result[entityId] = {
|
||||||
id: entityId,
|
id: entityId,
|
||||||
properties: {}
|
properties: {}
|
||||||
@ -313,6 +331,8 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(`Result:`, JSON.stringify(result));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user