import { Objects } from "@supersoniks/concorde/utils"; import DataProvider, { PublisherManager } from "../../utils/PublisherProxy"; import { ConnectedComponent, setSubscribable } from "./common"; const dynamicWatcherStore = Symbol("__onAssignDynamicWatcherStore__"); const dynamicWillUpdateHookedStore = Symbol( "__onAssignDynamicWillUpdateHooked__" ); function registerDynamicWatcher( instance: any, propertyName: string, onChange: () => void ) { const key = String(propertyName); ensureWillUpdateHook(instance); if (!instance[dynamicWatcherStore]) { Object.defineProperty(instance, dynamicWatcherStore, { value: new Map void>>(), enumerable: false, configurable: false, writable: false, }); } const watcherMap = instance[dynamicWatcherStore] as Map< string, Set<() => void> >; if (!watcherMap.has(key)) { watcherMap.set(key, new Set()); } const watchers = watcherMap.get(key)!; watchers.add(onChange); return () => { watchers.delete(onChange); if (watchers.size === 0) { watcherMap.delete(key); } }; } function ensureWillUpdateHook(instance: any) { const proto = Object.getPrototypeOf(instance); if (!proto || proto[dynamicWillUpdateHookedStore]) return; const originalWillUpdate = Object.prototype.hasOwnProperty.call( proto, "willUpdate" ) ? proto.willUpdate : Object.getPrototypeOf(proto)?.willUpdate; proto.willUpdate = function (changedProperties?: Map) { const handlers = this[dynamicWatcherStore] as | Map void>> | undefined; if (handlers && handlers.size > 0) { if (changedProperties && changedProperties.size > 0) { changedProperties.forEach((_value, dependency) => { const callbacks = handlers.get(String(dependency)); if (callbacks) { callbacks.forEach((cb) => cb()); } }); } else { handlers.forEach((callbacks) => callbacks.forEach((cb) => cb())); } } originalWillUpdate?.call(this, changedProperties); }; proto[dynamicWillUpdateHookedStore] = true; } function extractDynamicDependencies(path: string) { const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g]; const deps = new Set(); for (const pattern of patterns) { let match; while ((match = pattern.exec(path)) !== null) { const cleaned = cleanPlaceholder(match[1]); if (!cleaned) continue; const [root] = cleaned.split("."); if (root) deps.add(root); } } return Array.from(deps); } function cleanPlaceholder(value: string) { return value.trim().replace(/^this\./, ""); } function resolveDynamicPath(component: any, template: string) { let missing = false; const replaceValue = (_match: string, expression: string) => { const cleaned = cleanPlaceholder(expression); const resolved = getValueFromExpression(component, cleaned); if (resolved === undefined || resolved === null) { missing = true; return ""; } return `${resolved}`; }; const resolvedPath = template .replace(/\$\{([^}]+)\}/g, replaceValue) .replace(/\{\$([^}]+)\}/g, replaceValue) .trim(); if (missing || !resolvedPath.length) { return { ready: false, path: null }; } const segments = resolvedPath.split(".").filter(Boolean); if (segments.length === 0 || !segments[0]) { return { ready: false, path: null }; } return { ready: true, path: resolvedPath }; } function getValueFromExpression(component: any, expression: string) { if (!expression) return undefined; const segments = expression.split(".").filter(Boolean); if (segments.length === 0) return undefined; let current: unknown = component; for (const segment of segments) { if ( current === undefined || current === null || typeof current !== "object" ) { return undefined; } current = (current as Record)[segment]; } return current; } function getPublisherFromPath(path: string) { const segments = path.split(".").filter((segment) => segment.length > 0); if (segments.length === 0) return null; const dataProvider = segments.shift() || ""; if (!dataProvider) return null; let publisher = PublisherManager.get(dataProvider); if (!publisher) return null; publisher = Objects.traverse(publisher, segments); return publisher as DataProvider | null; } type Callback = (...values: unknown[]) => void; type PathConfiguration = { originalPath: string; dynamicDependencies: string[]; isDynamic: boolean; }; type Configuration = { callbacks: Set; publisher: DataProvider | null; onAssign: (value: unknown) => void; unsubscribePublisher: (() => void) | null; pathConfig: PathConfiguration; index: number; }; export function onAssign(...values: Array) { const pathConfigs: PathConfiguration[] = values.map((path) => { const dynamicDependencies = extractDynamicDependencies(path); return { originalPath: path, dynamicDependencies, isDynamic: dynamicDependencies.length > 0, }; }); return function ( target: unknown, _propertyKey: string, descriptor: PropertyDescriptor ) { setSubscribable(target); const stateKey = `__onAssign_state_${_propertyKey}__`; let callback: Callback; (target as ConnectedComponent).__onConnected__((component) => { const state = (component as any)[stateKey] || ((component as any)[stateKey] = { cleanupWatchers: [] as Array<() => void>, configurations: [] as Configuration[], }); // Nettoyage des watchers et configurations précédentes state.cleanupWatchers.forEach((cleanup: () => void) => cleanup()); state.cleanupWatchers = []; state.configurations.forEach((conf: Configuration) => { if (conf.unsubscribePublisher) { conf.unsubscribePublisher(); } }); state.configurations = []; const onAssignValues: unknown[] = []; const confs: Configuration[] = []; // Initialisation des configurations for (let i = 0; i < values.length; i++) { const pathConfig = pathConfigs[i]; const callbacks: Set = new Set(); const onAssign = (assignedValue: unknown) => { onAssignValues[i] = assignedValue; if ( onAssignValues.filter((v) => v !== null && v !== undefined) .length === values.length ) { callbacks.forEach((callback) => callback(...onAssignValues)); } }; confs.push({ publisher: null, onAssign, callbacks, unsubscribePublisher: null, pathConfig, index: i, }); } const subscribeToPath = ( conf: Configuration, resolvedPath: string | null ) => { // Désabonnement de l'ancien publisher if (conf.unsubscribePublisher) { conf.unsubscribePublisher(); conf.unsubscribePublisher = null; } // Réinitialiser la valeur pour ce chemin lors du changement onAssignValues[conf.index] = null; conf.publisher = null; if (!resolvedPath) { return; } const publisher = getPublisherFromPath(resolvedPath); if (!publisher) { return; } publisher.onAssign(conf.onAssign); conf.unsubscribePublisher = () => { publisher.offAssign(conf.onAssign); if (conf.publisher === publisher) { conf.publisher = null; } }; conf.publisher = publisher; }; const refreshSubscriptions = () => { for (const conf of confs) { if (conf.pathConfig.isDynamic) { const resolution = resolveDynamicPath( component, conf.pathConfig.originalPath ); if (!resolution.ready) { subscribeToPath(conf, null); continue; } subscribeToPath(conf, resolution.path); } else { subscribeToPath(conf, conf.pathConfig.originalPath); } } }; // Enregistrement des watchers pour les chemins dynamiques for (const conf of confs) { if (conf.pathConfig.isDynamic) { for (const dependency of conf.pathConfig.dynamicDependencies) { const unsubscribe = registerDynamicWatcher( component as Record, dependency, () => refreshSubscriptions() ); state.cleanupWatchers.push(unsubscribe); } } } // Initialisation du callback callback = descriptor.value.bind(component); for (const conf of confs) { conf.callbacks.add(callback); } // Initialisation des abonnements refreshSubscriptions(); state.configurations = confs; }); (target as ConnectedComponent).__onDisconnected__((component) => { const state = (component as any)[stateKey]; if (!state) return; state.cleanupWatchers.forEach((cleanup: () => void) => cleanup()); state.cleanupWatchers = []; state.configurations.forEach((conf: Configuration) => { if (conf.unsubscribePublisher) { conf.unsubscribePublisher(); } conf.callbacks.delete(callback); }); state.configurations = []; }); }; }