204 lines
5.9 KiB
TypeScript
204 lines
5.9 KiB
TypeScript
import { describe, test, expect, beforeEach } from '@jest/globals';
|
|
import { RhizomeNode, Lossless, createDelta, CollapsedDelta } from '@src';
|
|
import {
|
|
CustomResolver,
|
|
DependencyStates,
|
|
ResolverPlugin
|
|
} from '@src/views/resolvers/custom-resolvers';
|
|
import { PropertyTypes } from '@src/core/types';
|
|
|
|
describe('Edge Cases', () => {
|
|
let node: RhizomeNode;
|
|
let lossless: Lossless;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
lossless = new Lossless(node);
|
|
});
|
|
|
|
test('should handle null values', () => {
|
|
// Create a type-safe plugin that handles null/undefined values
|
|
class NullSafeLastWriteWinsPlugin extends ResolverPlugin<{ value: PropertyTypes | null, timestamp: number }, never> {
|
|
readonly dependencies = [] as const;
|
|
|
|
initialize() {
|
|
return { value: null, timestamp: 0 };
|
|
}
|
|
|
|
update(
|
|
currentState: { value: PropertyTypes | null, timestamp: number },
|
|
newValue?: PropertyTypes,
|
|
delta?: CollapsedDelta,
|
|
) {
|
|
if (newValue === undefined) return currentState;
|
|
if (!delta) return currentState;
|
|
if (delta.timeCreated > currentState.timestamp) {
|
|
return { value: newValue, timestamp: delta.timeCreated };
|
|
}
|
|
return currentState;
|
|
}
|
|
|
|
resolve(
|
|
state: { value: PropertyTypes | null, timestamp: number },
|
|
): PropertyTypes | undefined {
|
|
return state.value;
|
|
}
|
|
}
|
|
|
|
const resolver = new CustomResolver(lossless, {
|
|
value: new NullSafeLastWriteWinsPlugin()
|
|
});
|
|
|
|
lossless.ingestDelta(
|
|
createDelta('user1', 'host1')
|
|
.withTimestamp(1000)
|
|
.setProperty('test2', 'value', null, 'test')
|
|
.buildV1()
|
|
);
|
|
|
|
const results = resolver.resolve() || {};
|
|
const test1 = results['test2']
|
|
expect(test1).toBeDefined();
|
|
expect(test1?.properties.value).toBeNull();
|
|
});
|
|
|
|
test('should handle concurrent updates with same timestamp', () => {
|
|
// Custom plugin that handles concurrent updates with the same timestamp
|
|
class ConcurrentUpdatePlugin extends ResolverPlugin<{ value: PropertyTypes, timestamp: number }, never> {
|
|
readonly dependencies = [] as const;
|
|
|
|
initialize() {
|
|
return { value: '', timestamp: 0 };
|
|
}
|
|
|
|
update(
|
|
currentState: { value: PropertyTypes, timestamp: number },
|
|
newValue: PropertyTypes,
|
|
delta: CollapsedDelta,
|
|
_dependencies: DependencyStates
|
|
) {
|
|
if (delta.timeCreated > currentState.timestamp) {
|
|
return { value: newValue, timestamp: delta.timeCreated };
|
|
} else if (delta.timeCreated === currentState.timestamp) {
|
|
// For concurrent updates, prefer the one with the lexicographically smaller value
|
|
const currentVal = String(currentState.value);
|
|
const newVal = String(newValue);
|
|
return newVal < currentVal
|
|
? { value: newValue, timestamp: delta.timeCreated }
|
|
: currentState;
|
|
}
|
|
return currentState;
|
|
}
|
|
|
|
resolve(state: { value: PropertyTypes, timestamp: number }) {
|
|
return state.value;
|
|
}
|
|
}
|
|
|
|
const resolver = new CustomResolver(lossless, {
|
|
value: new ConcurrentUpdatePlugin()
|
|
});
|
|
|
|
// Two updates with the same timestamp
|
|
lossless.ingestDelta(
|
|
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();
|
|
// Should pick one of the values deterministically
|
|
expect(test2?.properties.value).toBeNull();
|
|
});
|
|
|
|
test('should handle very large numbers of updates', () => {
|
|
// Plugin that handles large numbers of updates efficiently
|
|
class CounterPlugin extends ResolverPlugin<{ count: number }, never> {
|
|
readonly dependencies = [] as const;
|
|
|
|
initialize() {
|
|
return { count: 0 };
|
|
}
|
|
|
|
update(
|
|
currentState: { count: number },
|
|
_newValue: PropertyTypes,
|
|
_delta: CollapsedDelta,
|
|
_dependencies: DependencyStates
|
|
) {
|
|
return { count: currentState.count + 1 };
|
|
}
|
|
|
|
resolve(
|
|
state: { count: number },
|
|
_dependencies: DependencyStates
|
|
): number {
|
|
return state.count;
|
|
}
|
|
}
|
|
|
|
const resolver = new CustomResolver(lossless, {
|
|
counter: new CounterPlugin()
|
|
});
|
|
|
|
// Add 1000 updates
|
|
for (let i = 0; i < 1000; i++) {
|
|
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();
|
|
// Should handle large numbers of updates efficiently
|
|
expect(test3?.properties.counter).toBe(1000); // Should count all 1000 updates
|
|
});
|
|
|
|
test('should handle missing properties gracefully', () => {
|
|
// No deltas added - should handle empty state
|
|
// Plugin that handles missing properties gracefully
|
|
class MissingPropertyPlugin extends ResolverPlugin<{ initialized: boolean }, never> {
|
|
readonly dependencies = [] as const;
|
|
|
|
initialize() {
|
|
return { initialized: true };
|
|
}
|
|
|
|
update(
|
|
currentState: { initialized: boolean },
|
|
) {
|
|
return currentState;
|
|
}
|
|
|
|
resolve(
|
|
state: { initialized: boolean }
|
|
): boolean {
|
|
return state.initialized;
|
|
}
|
|
}
|
|
|
|
const resolver = new CustomResolver(lossless, {
|
|
missing: new MissingPropertyPlugin()
|
|
});
|
|
|
|
const result = resolver.resolve();
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|