You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
13 KiB
354 lines
13 KiB
import type {GlobInstance} from "@/glob/lib";
|
|
// GlobInstance puede ser null cuando la sesión se usa solo para evaluación
|
|
// (sin pricing ni view resolution)
|
|
type GlobOrNull = GlobInstance | null;
|
|
import { JsonLogicEngine } from '@/jslg/lib';
|
|
import type {
|
|
Cat,
|
|
AttEffectiveState,
|
|
EvaluationResult,
|
|
AttPath,
|
|
DependencyGraph,
|
|
SessionListener, ObjPriceResult, View, Obj, Visual, ViewResolution, ConfigState
|
|
} from './types';
|
|
import {keyToAttPath, serializeAttPath} from './node_util.ts';
|
|
import { buildDependencyGraph } from './graph.ts';
|
|
import {buildConfigState, evaluateCat} from './engine.ts';
|
|
import { evaluateIncremental } from './incremental.ts';
|
|
import {computeObjPrice} from "@/vcen/lib/pricing.ts";
|
|
|
|
import {
|
|
resolveView as resolveViewFn,
|
|
resolveVisual as resolveVisualFn,
|
|
resolveDefaultView as resolveDefaultViewFn
|
|
} from './view';
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// SESSION
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Sesión de configuración en runtime.
|
|
*
|
|
* Encapsula el `Cat`, el `DependencyGraph`, el `EvaluationResult` actual,
|
|
* el motor de pricing y el motor visual.
|
|
*
|
|
* ## Ciclo de vida
|
|
*
|
|
* ```
|
|
* const session = createSession(cat, glob);
|
|
* session.setValue(path, value); // el usuario cambia un valor
|
|
* session.getValue(path); // leer valor actual
|
|
* session.getEffectiveState(path); // leer estado efectivo
|
|
* session.getPrice('ob:bmw430'); // precio del objeto configurado
|
|
* session.resolveDefaultView(visual, obj); // vista activa del objeto
|
|
* session.reset(); // volver al estado inicial
|
|
* ```
|
|
*
|
|
* ## Observers
|
|
*
|
|
* ```
|
|
* const unsub = session.subscribe(result => { ... });
|
|
* unsub(); // cancelar suscripción
|
|
* ```
|
|
*/
|
|
export class Session {
|
|
private readonly cat: Cat;
|
|
private readonly graph: DependencyGraph;
|
|
private readonly engine: JsonLogicEngine;
|
|
private readonly glob: GlobOrNull;
|
|
private readonly initialResult: EvaluationResult;
|
|
private current: EvaluationResult;
|
|
private readonly listeners: Set<SessionListener> = new Set();
|
|
private configState: ConfigState;
|
|
private readonly userValues: Map<string, unknown> = new Map();
|
|
|
|
constructor(
|
|
cat: Cat,
|
|
glob: GlobOrNull = null,
|
|
engine: JsonLogicEngine | null = null,
|
|
) {
|
|
this.cat = cat;
|
|
this.glob = glob;
|
|
this.engine = engine ?? new JsonLogicEngine();
|
|
this.graph = buildDependencyGraph(cat, this.engine);
|
|
|
|
const initial = evaluateCat(cat, this.engine);
|
|
this.initialResult = initial;
|
|
this.current = initial;
|
|
this.configState = buildConfigState(cat);
|
|
}
|
|
|
|
// ── Lectura ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Valor actual de un atributo.
|
|
* Si el motor ha derivado un valor, devuelve el derivado.
|
|
*/
|
|
getValue(path: AttPath): unknown {
|
|
const key = serializeAttPath(path);
|
|
const es = this.current.effectiveState.get(key);
|
|
return es?.derivedValue?.value ?? this.findAttValue(key);
|
|
}
|
|
|
|
/**
|
|
* Estado efectivo de un atributo (available, hidden, required, forbidden,
|
|
* forbiddenValues, messages, derivedValue).
|
|
*/
|
|
getEffectiveState(path: AttPath): AttEffectiveState | undefined {
|
|
return this.current.effectiveState.get(serializeAttPath(path));
|
|
}
|
|
|
|
/** Lista de paths con errores en el estado actual. */
|
|
getErrors(): string[] {
|
|
return this.current.errors;
|
|
}
|
|
|
|
/** Lista de paths con warnings en el estado actual. */
|
|
getWarnings(): string[] {
|
|
return this.current.warnings;
|
|
}
|
|
|
|
/** `true` si la configuración actual es válida. */
|
|
isValid(): boolean {
|
|
return this.current.valid;
|
|
}
|
|
|
|
/** EvaluationResult completo actual — para la UI que necesita el estado completo. */
|
|
getResult(): EvaluationResult {
|
|
return this.current;
|
|
}
|
|
|
|
// ── Pricing ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Calcula el precio de un `Obj` concreto en el estado actual de la sesión.
|
|
*
|
|
* Usa el `ConfigState` actual (con los valores modificados por el usuario)
|
|
* y la instancia `glob` para resolver `LingString` y formatear moneda.
|
|
*
|
|
* @param objId - ID del objeto a calcular
|
|
* @returns `ObjPriceResult` o `undefined` si el objId no existe en el Cat
|
|
*/
|
|
getPrice(objId: string): ObjPriceResult | undefined {
|
|
if (!this.glob) return undefined;
|
|
const obj = this.cat.objs.find(o => o.id === objId);
|
|
if (!obj) return undefined;
|
|
|
|
return computeObjPrice(
|
|
obj,
|
|
this.cat.opts,
|
|
this.cat.pricingDefaults,
|
|
this.configState,
|
|
this.glob,
|
|
this.engine,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Calcula el precio de todos los `Obj` del Cat en el estado actual.
|
|
*
|
|
* @returns Array de `ObjPriceResult`, uno por cada Obj del Cat
|
|
*/
|
|
getAllPrices(): ObjPriceResult[] {
|
|
if (!this.glob) return [];
|
|
return this.cat.objs.map(obj =>
|
|
computeObjPrice(obj, this.cat.opts, this.cat.pricingDefaults, this.configState, this.glob!, this.engine)
|
|
);
|
|
}
|
|
|
|
// ── Visual ────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Resuelve una `View` concreta para un `Obj`.
|
|
*
|
|
* Aplica la cadena de herencia de `basePath` y `fallback`:
|
|
* ViewConfig → secVisual → objVisual → Cat.visualDefaults
|
|
*
|
|
* @param view - Vista a resolver
|
|
* @param obj - Objeto configurado
|
|
* @param secVisual - Visual de la Sec que contiene la vista (si aplica)
|
|
* @param objVisual - Visual del Obj (si aplica)
|
|
*/
|
|
resolveView(
|
|
view: View,
|
|
obj: Obj,
|
|
secVisual?: Visual,
|
|
objVisual?: Visual,
|
|
): ViewResolution | undefined {
|
|
if (!this.glob) return undefined;
|
|
return resolveViewFn(
|
|
view, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resuelve todas las vistas de un `Visual` para un `Obj`.
|
|
*
|
|
* @param visual - Visual a resolver (de un Obj o Sec)
|
|
* @param obj - Objeto configurado
|
|
* @param secVisual - Visual de la Sec (si aplica)
|
|
* @param objVisual - Visual del Obj (si aplica)
|
|
*/
|
|
resolveVisual(
|
|
visual: Visual,
|
|
obj: Obj,
|
|
secVisual?: Visual,
|
|
objVisual?: Visual,
|
|
): ViewResolution[] {
|
|
if (!this.glob) return [];
|
|
return resolveVisualFn(
|
|
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resuelve la vista activa por defecto de un `Visual`.
|
|
* Usa `defaultViewId` si está definido; si no, la primera vista.
|
|
*
|
|
* @returns `ViewResolution` o `undefined` si el Visual no tiene vistas
|
|
*/
|
|
resolveDefaultView(
|
|
visual: Visual,
|
|
obj: Obj,
|
|
secVisual?: Visual,
|
|
objVisual?: Visual,
|
|
): ViewResolution | undefined {
|
|
if (!this.glob) return undefined;
|
|
return resolveDefaultViewFn(
|
|
visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine,
|
|
);
|
|
}
|
|
|
|
// ── Mutación ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Cambia el valor de un atributo y re-evalúa incrementalmente.
|
|
* Notifica a todos los listeners con el nuevo resultado.
|
|
*
|
|
* @returns El nuevo `EvaluationResult` tras el cambio.
|
|
*/
|
|
setValue(path: AttPath, value: unknown): EvaluationResult {
|
|
this.userValues.set(serializeAttPath(path), value);
|
|
|
|
const next = evaluateIncremental(
|
|
this.cat,
|
|
this.graph,
|
|
this.current,
|
|
{ path, value },
|
|
this.engine,
|
|
);
|
|
|
|
this.current = next;
|
|
this.configState = this.buildCurrentConfigState();
|
|
this.notify();
|
|
return next;
|
|
}
|
|
|
|
/**
|
|
* Vuelve al estado inicial (valores del Cat original, evaluación completa).
|
|
* Notifica a todos los listeners.
|
|
*/
|
|
reset(): EvaluationResult {
|
|
this.userValues.clear();
|
|
this.current = this.initialResult;
|
|
this.configState = buildConfigState(this.cat);
|
|
this.notify();
|
|
return this.current;
|
|
}
|
|
|
|
// ── Observers ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Suscribe un listener que se llama cada vez que cambia el estado.
|
|
*
|
|
* @returns Función para cancelar la suscripción.
|
|
*
|
|
* @example
|
|
* const unsub = session.subscribe(result => render(result));
|
|
* unsub();
|
|
*/
|
|
subscribe(listener: SessionListener): () => void {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
}
|
|
|
|
// ── Internals ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Reconstruye el ConfigState a partir de:
|
|
* 1. Valores base del Cat
|
|
* 2. Valores establecidos por el usuario (userValues)
|
|
* 3. Valores derivados del resultado actual (derivedValues — máxima prioridad)
|
|
*/
|
|
private buildCurrentConfigState(): ConfigState {
|
|
const state = buildConfigState(this.cat);
|
|
|
|
for (const [key, value] of this.userValues) {
|
|
state[key] = value;
|
|
}
|
|
|
|
for (const [key, es] of this.current.effectiveState) {
|
|
if (es.derivedValue !== undefined) {
|
|
state[key] = es.derivedValue.value;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
private notify() {
|
|
for (const listener of this.listeners) {
|
|
listener(this.current);
|
|
}
|
|
}
|
|
|
|
private findAttValue(key: string): unknown {
|
|
const parts = key.split('/');
|
|
|
|
if (parts.length === 2) {
|
|
return this.cat.atts.find(a => a.id === parts[1])?.value.value;
|
|
}
|
|
if (parts.length === 3) {
|
|
const obj = this.cat.objs.find(o => o.id === parts[1]);
|
|
return obj?.atts.find(a => a.id === parts[2])?.value.value;
|
|
}
|
|
if (parts.length === 4) {
|
|
const obj = this.cat.objs.find(o => o.id === parts[1]);
|
|
const sec = obj?.secs.find(s => s.id === parts[2]);
|
|
return sec?.atts.find(a => a.id === parts[3])?.value.value;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// FACTORY
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Crea una nueva sesión de configuración.
|
|
*
|
|
* @param cat - Catálogo a configurar
|
|
* @param glob - Instancia de GlobInstance para pricing y localización
|
|
* @param engine - Instancia JsonLogicEngine (null = nueva interna)
|
|
*
|
|
* @throws DependencyCycleError si el Cat tiene ciclos en sus reglas
|
|
*
|
|
* @example
|
|
* const session = createSession(cat, glob);
|
|
* session.setValue(['ct:cars', 'ob:bmw', 'at:acabado'], 'lujo');
|
|
* session.getPrice('ob:bmw');
|
|
* session.isValid();
|
|
*/
|
|
export function createSession(
|
|
cat: Cat,
|
|
glob: GlobInstance | null = null,
|
|
engine: JsonLogicEngine | null = null,
|
|
): Session {
|
|
return new Session(cat, glob, engine);
|
|
}
|