hard-won battle to fix view resolution

This commit is contained in:
Lentil Hoffman 2025-06-25 16:43:13 -05:00
parent bdc6958b49
commit 29b1b8bb9a
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
3 changed files with 62 additions and 65 deletions

View File

@ -1,5 +1,4 @@
import { PropertyID, PropertyTypes } from "@src/core/types"; import { PropertyID, PropertyTypes } from "@src/core/types";
import { CollapsedDelta } from "@src/views/lossless";
import { ResolverPlugin, DependencyStates } from "../plugin"; import { ResolverPlugin, DependencyStates } from "../plugin";
type MaxPluginState = { type MaxPluginState = {
@ -29,7 +28,6 @@ export class MaxPlugin<Target extends PropertyID> extends ResolverPlugin<MaxPlug
update( update(
currentState: MaxPluginState, currentState: MaxPluginState,
newValue?: PropertyTypes, newValue?: PropertyTypes,
_delta?: CollapsedDelta,
): MaxPluginState { ): MaxPluginState {
const numValue = newValue as number; const numValue = newValue as number;
if (currentState.max === undefined || numValue > currentState.max) { if (currentState.max === undefined || numValue > currentState.max) {
@ -40,7 +38,6 @@ export class MaxPlugin<Target extends PropertyID> extends ResolverPlugin<MaxPlug
resolve( resolve(
state: MaxPluginState, state: MaxPluginState,
_dependencies?: DependencyStates
): PropertyTypes | undefined { ): PropertyTypes | undefined {
return state.max; return state.max;
} }

View File

@ -27,7 +27,7 @@ export class MinPlugin<Target extends PropertyID> extends ResolverPlugin<MinPlug
update( update(
currentState: MinPluginState, currentState: MinPluginState,
newValue: PropertyTypes, newValue?: PropertyTypes,
): MinPluginState { ): MinPluginState {
const numValue = newValue as number; const numValue = newValue as number;
if (currentState.min === undefined || numValue < currentState.min) { if (currentState.min === undefined || numValue < currentState.min) {

View File

@ -64,7 +64,7 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
* @param propertyId The key by which a plugin is registered * @param propertyId The key by which a plugin is registered
* @returns The base name of the plugin * @returns The base name of the plugin
*/ */
pluginBasenameFromKey(propertyId: PropertyID): PropertyID { pluginBasenameFromKey(propertyId: string): string {
return this.config[propertyId]?.name || propertyId; return this.config[propertyId]?.name || propertyId;
} }
@ -73,12 +73,22 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
* @param alias The alias of the plugin * @param alias The alias of the plugin
* @returns The key by which it is registered * @returns The key by which it is registered
*/ */
pluginKeyFromBasename(alias: PropertyID): PropertyID { pluginKeyFromBasename(name: string): string {
const entry = Object.entries(this.config).find(([_, plugin]) => plugin.name === alias); const entry = Object.entries(this.config).find(([_, plugin]) => plugin.name === name);
if (!entry) return alias; if (!entry) return name;
return entry[0]; return entry[0];
} }
private logGraph(): void {
// Log the final dependency graph
const graphLog: Record<string, string[]> = {};
this.dependencyGraph.forEach((deps, plugin) => {
graphLog[plugin] = Array.from(deps);
});
debug(`Dependency graph: ${JSON.stringify(graphLog, null, 2)}`);
}
/** /**
* Build the dependency graph for all plugins. * Build the dependency graph for all plugins.
* We'll use the basenames of the plugins in the graph. * We'll use the basenames of the plugins in the graph.
@ -87,16 +97,15 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
debug('Building dependency graph...'); debug('Building dependency graph...');
// Initialize the graph with all plugins // Initialize the graph with all plugins
Object.keys(this.config).forEach(propertyId => { Object.keys(this.config).forEach(pluginKey => {
const pluginId = this.pluginBasenameFromKey(propertyId); this.dependencyGraph.set(pluginKey, new Set());
this.dependencyGraph.set(pluginId, new Set()); debug(`Added plugin node: ${pluginKey}`);
debug(`Added plugin node: ${pluginId} (from property: ${propertyId})`);
}); });
debug('Processing plugin dependencies...'); debug('Processing plugin dependencies...');
// Add edges based on dependencies // Add edges based on dependencies
Object.entries(this.config).forEach(([propertyId, plugin]) => { Object.entries(this.config).forEach(([pluginKey, plugin]) => {
const pluginId = plugin.name || propertyId; const pluginId = plugin.name || pluginKey;
const deps = plugin.dependencies || []; const deps = plugin.dependencies || [];
if (deps.length === 0) { if (deps.length === 0) {
@ -108,29 +117,27 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
deps.forEach((depId: string) => { deps.forEach((depId: string) => {
// This dependency may have an alias in our current config // This dependency may have an alias in our current config
const depKey = this.pluginKeyFromBasename(depId); const depKey = this.pluginKeyFromBasename(depId);
debug(`Processing dependency: ${depId} (resolved to key: ${depKey}) for plugin ${pluginId}`); debug(`Processing dependency ${depKey} for plugin ${pluginKey}`);
if (!this.config[depKey]) { if (!this.config[depKey]) {
const errorMsg = `Dependency ${depId} not found for plugin ${propertyId}`; // TODO: This could still be a property, not a plugin
const errorMsg = `Dependency ${depKey} not found for plugin ${pluginKey}`;
debug(`Error: ${errorMsg}`); debug(`Error: ${errorMsg}`);
throw new Error(errorMsg); throw new Error(errorMsg);
} }
// Add the dependency edge // Add the dependency edge
this.dependencyGraph.get(depId)?.add(pluginId); const dep = this.dependencyGraph.get(depKey)
debug(`Added edge: ${depId} -> ${pluginId}`); if (!dep) {
throw new Error(`Dependency ${depKey} not found in dependency graph`);
}
dep.add(pluginKey);
}); });
}); });
// Log the final dependency graph
const graphLog: Record<string, string[]> = {};
this.dependencyGraph.forEach((deps, plugin) => {
graphLog[plugin] = Array.from(deps);
});
debug('Dependency graph construction complete'); debug('Dependency graph construction complete');
debug(`Config: ${JSON.stringify(this.config, null, 2)}`); debug(`Config: ${JSON.stringify(this.config, null, 2)}`);
debug(`Dependency graph: ${JSON.stringify(graphLog, null, 2)}`); this.logGraph();
} }
/** /**
@ -188,36 +195,28 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
/** /**
* Get the resolved states of all dependencies for a plugin * Get the resolved states of all dependencies for a plugin
* @param entityState The state of the entity * @param entityPluginStates The state of the entity
* @param dependencies The dependencies to resolve * @param dependencies The dependencies to resolve
* *
*/ */
private getDependencyStates( private getDependencyStates(
entityState: EntityState, entityPluginStates: EntityState,
plugin: ResolverPlugin<unknown, string> pluginKey: string
): DependencyStates { ): DependencyStates {
const dependencyStates = {} as DependencyStates; const plugin = this.config[pluginKey];
if (!plugin) throw new Error(`Plugin ${pluginKey} not found`);
for (const depId of plugin.dependencies || []) { const dependencyStates: DependencyStates = {};
const depKey = this.pluginKeyFromBasename(depId);
for (const depKey of this.executionOrder) {
if (depKey === pluginKey) continue;
const depPlugin = this.config[depKey]; const depPlugin = this.config[depKey];
if (depPlugin) {
// TODO: If this is not a plugin, see if it's an entity property, and include it if (!entityPluginStates[depKey]) {
dependencyStates[depKey] = depPlugin.initialize(dependencyStates);
const depValue = entityState[depKey]; entityPluginStates[depKey] = dependencyStates[depKey];
debug(`depId: ${depId}, depKey: ${depKey}, depPlugin: ${JSON.stringify(depPlugin)}, depValue: ${JSON.stringify(depValue)}`) }
if (depValue) { dependencyStates[depKey] = depPlugin.resolve(entityPluginStates[depKey], dependencyStates);
// Resolve the dependency's dependencies first
const depDependencies = this.getDependencyStates(
entityState,
depPlugin
);
// Resolve the dependency's state
dependencyStates[depId] = depPlugin.resolve(
depValue,
depDependencies
);
} }
} }
@ -230,14 +229,13 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
} }
const entityState = acc[entityId]; const entityState = acc[entityId];
for (const pluginId of this.executionOrder) { for (const pluginKey of this.executionOrder) {
const pluginKey = this.pluginKeyFromBasename(pluginId);
const plugin = this.config[pluginKey]; const plugin = this.config[pluginKey];
if (!plugin) throw new Error(`Plugin for property ${pluginId} not found`); if (!plugin) throw new Error(`Plugin ${pluginKey} not found`);
// We need to resolve dependencies, including entity properties that are not plugins. // We need to resolve dependencies, including entity properties that are not plugins.
const dependencies = this.getDependencyStates(entityState, plugin); const dependencies = this.getDependencyStates(entityState, pluginKey);
debug('Dependencies for', pluginId, ':', JSON.stringify(dependencies)); debug('Dependencies for', pluginKey, ':', JSON.stringify(dependencies));
// Initialize the plugin if it hasn't been initialized yet // Initialize the plugin if it hasn't been initialized yet
entityState[pluginKey] = entityState[pluginKey] ?? plugin.initialize(dependencies); entityState[pluginKey] = entityState[pluginKey] ?? plugin.initialize(dependencies);
@ -276,6 +274,8 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
// It's possible that there are multiple deltas in this set with the same property ID. // It's possible that there are multiple deltas in this set with the same property ID.
// That can only happen if they are part of a transaction. Otherwise this function is // That can only happen if they are part of a transaction. Otherwise this function is
// only called once per delta, per entity affected. // only called once per delta, per entity affected.
// TODO: More flexible/robust error handling protocols?
// Some views might be more tolerant of errors than others.
throw new Error(`Duplicate property ID ${propertyId} found in delta ${delta.id}`); throw new Error(`Duplicate property ID ${propertyId} found in delta ${delta.id}`);
} }
deltaPropertyValues[propertyId] = pointer[propertyId]; deltaPropertyValues[propertyId] = pointer[propertyId];
@ -290,9 +290,10 @@ 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); const dependencies = this.getDependencyStates(entityState, pluginKey);
entityState[pluginKey] = plugin.applyUpdate(pluginState, propertyValue, updateDelta, dependencies); entityState[pluginKey] = plugin.applyUpdate(pluginState, propertyValue, updateDelta, dependencies);
debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey])); debugState(`Updated state for entity ${entityId} plugin ${pluginKey}:`,
JSON.stringify(entityState[pluginKey]));
} }
return acc; return acc;
@ -312,21 +313,20 @@ export class CustomResolver extends Lossy<Accumulator, Result> {
properties: {} properties: {}
}; };
for (const propertyId of this.executionOrder) { for (const pluginKey of this.executionOrder) {
const pluginKey = this.pluginKeyFromBasename(propertyId);
const plugin = this.config[pluginKey]; const plugin = this.config[pluginKey];
if (!plugin) throw new Error(`Plugin for property ${propertyId} not found`); if (!plugin) throw new Error(`Plugin ${pluginKey} not found`);
debug(`Processing property: ${propertyId} (key: ${pluginKey})`); debug(`Processing property: ${pluginKey}`);
const dependencies = this.getDependencyStates(acc[entityId], plugin); const dependencies = this.getDependencyStates(acc[entityId], pluginKey);
debug(`Dependencies for ${propertyId}:`, JSON.stringify(dependencies)); debug(`Dependencies for ${pluginKey}:`, JSON.stringify(dependencies));
const state = acc[entityId][pluginKey] || plugin.initialize(dependencies); const state = acc[entityId][pluginKey] || plugin.initialize(dependencies);
debug(`State for ${propertyId}:`, JSON.stringify(state)); debug(`State for ${pluginKey}:`, JSON.stringify(state));
const resolvedValue = plugin.resolve(state, dependencies); const resolvedValue = plugin.resolve(state, dependencies);
if (resolvedValue === undefined) throw new Error(`Resolved value for property ${propertyId} is undefined`) if (resolvedValue === undefined) throw new Error(`Resolved value for property ${pluginKey} is undefined`)
debug(`Resolved value for ${propertyId}:`, resolvedValue); debug(`Resolved value for ${pluginKey}:`, resolvedValue);
result[entityId].properties[pluginKey] = resolvedValue; result[entityId].properties[pluginKey] = resolvedValue;
} }
} }