refactor: optimize lossless view and improve code quality

- Optimize pointer handling and entity reference tracking in Lossless view
- Improve type safety with proper TypeScript types
- Add debug logging for better troubleshooting
- Clean up imports and unused variables
- Update test cases for DeltaBuilder
This commit is contained in:
Lentil Hoffman 2025-06-25 11:18:52 -05:00
parent d7c4fda93e
commit d0941f417e
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
11 changed files with 83 additions and 48 deletions

View File

@ -46,8 +46,8 @@ describe('DeltaBuilder', () => {
// Verify that the entity property resolves correctly
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new TimestampResolver(lossless);
lossless.ingestDelta(delta);
const result = lossy.resolve();
expect(result).toBeDefined();
expect(result!['entity-1'].properties.name).toBe('Test Entity');
@ -71,8 +71,8 @@ describe('DeltaBuilder', () => {
// Verify that the entity property resolves correctly
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new TimestampResolver(lossless);
lossless.ingestDelta(delta);
const result = lossy.resolve();
expect(result).toBeDefined();
expect(result!['entity-1'].properties.name).toBe('Test Entity');
@ -171,8 +171,8 @@ describe('DeltaBuilder', () => {
expect(delta.pointers).toHaveProperty('type', 'follows');
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new TimestampResolver(lossless);
lossless.ingestDelta(delta);
const result = lossy.resolve([relId]);
expect(result).toBeDefined();
expect(result![relId]).toMatchObject({
@ -201,8 +201,8 @@ describe('DeltaBuilder', () => {
expect(delta.pointers).toHaveProperty('version', 1);
const lossless = new Lossless(node);
lossless.ingestDelta(delta);
const lossy = new TimestampResolver(lossless);
lossless.ingestDelta(delta);
const result = lossy.resolve([relId]);
expect(result).toBeDefined();
expect(result![relId]).toMatchObject({

View File

@ -351,8 +351,8 @@ describe('Negation System', () => {
test('should handle self-referential entities in negations', () => {
// Create a delta that references itself
const selfRefDelta = createDelta('user1', 'host1')
.setProperty('node1', 'parent', 'node1')
.setProperty('node1', 'child', 'node1') // Self-reference
.setProperty('node1', 'parent', 'node1', 'node-as-parent')
.setProperty('node1', 'child', 'node1', 'node-as-child') // Self-reference
.buildV1();
const negationDelta = createDelta('admin', 'host1').negate(selfRefDelta.id).buildV1();

View File

@ -4,7 +4,7 @@ import { Lossless, LosslessViewOne } from "../../../src/views/lossless";
import { Lossy } from "../../../src/views/lossy";
import { RhizomeNode } from "../../../src/node";
import { valueFromCollapsedDelta } from "../../../src/views/resolvers/aggregation-resolvers";
import { lastValueFromDeltas } from "../../../src/views/resolvers/timestamp-resolvers";
import { latestFromCollapsedDeltas } from "../../../src/views/resolvers/timestamp-resolvers";
import { createDelta } from "../../../src/core/delta-builder";
const debug = Debug('rz:test:lossy');
@ -18,8 +18,16 @@ type Summary = {
roles: Role[];
};
class Summarizer extends Lossy<Summary, Summary> {
class Summarizer extends Lossy<Summary> {
private readonly debug: debug.Debugger;
constructor(lossless: Lossless) {
super(lossless);
this.debug = Debug('rz:test:lossy:summarizer');
}
initializer(): Summary {
this.debug('Initializing new summary');
return {
roles: []
};
@ -30,23 +38,53 @@ class Summarizer extends Lossy<Summary, Summary> {
// TODO: Prove with failing test
reducer(acc: Summary, cur: LosslessViewOne): Summary {
this.debug(`Processing view for entity ${cur.id} (referenced as: ${cur.referencedAs.join(', ')})`);
this.debug(`lossless view:`, JSON.stringify(cur));
if (cur.referencedAs.includes("role")) {
const {delta, value: actor} = lastValueFromDeltas("actor", cur.propertyDeltas["actor"]) ?? {};
if (!delta) throw new Error('expected to find delta');
if (!actor) throw new Error('expected to find actor');
this.debug(`Found role entity: ${cur.id}`);
const actorDeltas = cur.propertyDeltas["actor"];
this.debug(`Found ${actorDeltas?.length ?? 0} actor deltas`);
const {delta, value: actor} = latestFromCollapsedDeltas("actor", actorDeltas) ?? {};
if (!delta) {
this.debug('No delta found for actor property');
throw new Error('expected to find delta');
}
if (!actor) {
this.debug('No actor value found in deltas');
throw new Error('expected to find actor');
}
this.debug(`Found actor: ${actor}`);
const film = valueFromCollapsedDelta("film", delta);
if (!film) throw new Error('expected to find film');
acc.roles.push({
if (!film) {
this.debug('No film property found in delta');
throw new Error('expected to find film');
}
this.debug(`Found film: ${film}`);
const role = {
role: cur.id,
actor,
film
});
};
acc.roles.push(role);
this.debug(`Added role: ${JSON.stringify(role)}`);
}
this.debug(`Updated accumulator: ${JSON.stringify(acc, null, 2)}`);
return acc;
}
resolver(acc: Summary): Summary {
this.debug(`Resolving summary with ${acc.roles.length} roles`);
return acc;
}
}

View File

@ -2,6 +2,9 @@ import { DeltaV1, DeltaV2 } from './delta';
import { randomUUID } from 'crypto';
import { PropertyTypes } from './types';
import { PointersV2 } from './delta';
import Debug from 'debug';
const debug = Debug('rz:delta-builder');
/**
* A fluent builder for creating Delta objects with proper validation and type safety.
@ -77,9 +80,11 @@ export class DeltaBuilder {
addPointer(localContext: string, target: string | number | boolean | null, targetContext?: string): this {
const pointerTarget = (targetContext && typeof target === 'string')
? { [target]: targetContext } : target;
// Prevent duplicate primitive properties with the same key
if (this.pointers[localContext] &&
JSON.stringify(this.pointers[localContext]) !== JSON.stringify(pointerTarget)
) {
debug(`Pointer for '${localContext}' already exists with different value: ${JSON.stringify(this.pointers[localContext])} !== ${JSON.stringify(pointerTarget)}`);
throw new Error(`Pointer for ${localContext} already exists with different value`);
}
this.pointers[localContext] = pointerTarget;

View File

@ -1,4 +1,4 @@
import Docker, { DockerOptions } from 'dockerode';
import Docker from 'dockerode';
import * as path from 'path';
import { promises as fs } from 'fs';
import * as tar from 'tar-fs';
@ -63,9 +63,9 @@ export class ImageManager implements IImageManager {
debug('Created build context tar stream');
testImageBuildPromise = new Promise<void>((resolve, reject) => {
const log = (...args: any[]) => {
const log = (...args: unknown[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
debug(message);
};
@ -111,7 +111,7 @@ export class ImageManager implements IImageManager {
// Log any other non-empty JSON objects
log(`[Docker Build] ${JSON.stringify(json)}`);
}
} catch (e) {
} catch (_e) {
// If not JSON, log as plain text if not empty
if (line.trim()) {
log(`[Docker Build] ${line}`);

View File

@ -1,4 +1,4 @@
import Docker, { Container } from 'dockerode';
import { Container } from 'dockerode';
import { IStatusManager } from './interfaces';
import { NodeHandle, NodeStatus } from '../../types';
import Debug from 'debug';

View File

@ -269,7 +269,7 @@ export class QueryEngine {
case 'primitive': {
// Use last-write-wins for primitives
const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated);
for (let delta of deltasSorted) {
for (const delta of deltasSorted) {
const primitiveValue = this.extractPrimitiveValue(delta, propertyId);
if (primitiveValue !== null) {
obj[propertyId] = primitiveValue;

View File

@ -72,7 +72,7 @@ class LosslessEntity {
export class Lossless {
domainEntities = new LosslessEntityMap();
transactions: Transactions;
referencedAs = new Map<string, Set<DomainEntityID>>();
referencedAs = new Map<string, Set<string>>();
eventStream = new EventEmitter();
// Track all deltas by ID for negation processing
@ -155,19 +155,6 @@ export class Lossless {
}
}
for (const {target, localContext} of delta.pointers) {
if (typeof target === "string" && this.domainEntities.has(target)) {
if (this.domainEntities.has(target)) {
let referencedAs = this.referencedAs.get(localContext);
if (!referencedAs) {
referencedAs = new Set<string>();
this.referencedAs.set(localContext, referencedAs);
}
referencedAs.add(target);
}
}
}
const transactionId = this.transactions.ingestDelta(delta, targets);
if (!transactionId) {
@ -232,8 +219,8 @@ export class Lossless {
const ent = this.domainEntities.get(id);
if (!ent) continue;
const referencedAs = new Set<string>();
const propertyDeltas: {
[key: PropertyID]: CollapsedDelta[]
} = {};
@ -272,15 +259,11 @@ export class Lossless {
const pointers: CollapsedPointer[] = [];
for (const {localContext, target, targetContext} of delta.pointers) {
if (targetContext) {
// Only store primitive pointers in the collapsed delta
continue;
}
pointers.push({[localContext]: target});
for (const {localContext, target} of delta.pointers) {
if (target === ent.id) {
referencedAs.add(localContext);
}
pointers.push({[localContext]: target});
}
visibleDeltas.push({
@ -295,6 +278,14 @@ export class Lossless {
}
}
if (this.referencedAs.has(ent.id)) {
for (const ref of referencedAs) {
this.referencedAs.get(ent.id)!.add(ref);
}
} else {
this.referencedAs.set(ent.id, referencedAs);
}
// Only include entity in view if it has visible deltas
if (hasVisibleDeltas) {
view[ent.id] = {

View File

@ -47,6 +47,7 @@ export abstract class Lossy<Accumulator, Result = Accumulator> {
return this.deltaFilter(delta);
};
const losslessPartial = this.lossless.compose([entityId], combinedFilter);
debug(`Lossless partial for entity ${entityId}:`, JSON.stringify(losslessPartial));
if (!losslessPartial) {
// This should not happen; this should only be called after the lossless view has been updated

View File

@ -199,7 +199,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
*/
reducer(acc: Accumulator, {id: entityId, propertyDeltas}: LosslessViewOne): Accumulator {
debug(`Processing deltas for entity: ${entityId}`);
debug('Property deltas:', JSON.stringify(propertyDeltas, null, 2));
debug('Property deltas:', JSON.stringify(propertyDeltas));
if (!acc[entityId]) {
acc[entityId] = {};
@ -246,7 +246,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
// Update the plugin state with the new delta
entityState[pluginKey] = plugin.update(pluginState, propertyValue, updateDelta, dependencies);
debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey], null, 2));
debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey]));
}
return acc;
@ -254,7 +254,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
resolver(acc: Accumulator, entityIds: DomainEntityID[]) {
const result: Result = {};
debug('Initial accumulator state:', JSON.stringify(acc, null, 2));
debug('Initial accumulator state:', JSON.stringify(acc));
for (const entityId in acc) {
if (!entityIds.includes(entityId)) continue;
@ -270,9 +270,9 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
debug(`Processing property: ${propertyId} (key: ${pluginKey})`);
const dependencies = this.getDependencyStates(acc[entityId], plugin);
debug(`Dependencies for ${propertyId}:`, JSON.stringify(dependencies, null, 2));
debug(`Dependencies for ${propertyId}:`, JSON.stringify(dependencies));
const state = acc[entityId][pluginKey] || plugin.initialize(dependencies);
debug(`State for ${propertyId}:`, JSON.stringify(state, null, 2));
debug(`State for ${propertyId}:`, JSON.stringify(state));
const resolvedValue = plugin.resolve(state, dependencies);
if (resolvedValue === undefined) throw new Error(`Resolved value for property ${propertyId} is undefined`)

View File

@ -158,7 +158,7 @@ export class LexicographicTimestampResolver extends TimestampResolver {
}
// Resolve a value for an entity by last write wins
export function lastValueFromDeltas(
export function latestFromCollapsedDeltas(
key: string,
deltas?: CollapsedDelta[]
): {