import { Objects } from "@supersoniks/concorde/utils"; import DataProvider, { PublisherManager } from "../../utils/PublisherProxy"; import { ConnectedComponent, setSubscribable } from "./common"; const dynamicWatcherStore = Symbol("__bindDynamicWatcherStore__"); const dynamicWillUpdateHookedStore = Symbol("__bindDynamicWillUpdateHooked__"); 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; } export function bind(path: string, options?: { reflect?: boolean }) { const reflect = options?.reflect ?? false; const dynamicDependencies = extractDynamicDependencies(path); const isDynamicPath = dynamicDependencies.length > 0; return function (target: unknown, propertyKey: string) { if (!target) return; setSubscribable(target); const stateKey = `__bind_state_${propertyKey}`; const publisherKey = `__bind_${propertyKey}_publisher__`; const isUpdatingFromPublisherKey = reflect ? `__bind_${propertyKey}_updating_from_publisher__` : null; if (reflect) { const existingDescriptor = Object.getOwnPropertyDescriptor( target as any, propertyKey ); const internalValueKey = `__bind_${propertyKey}_value__`; const reflectUpdateFlagKey = `__bind_${propertyKey}_updating_from_publisher__`; const initialValue = existingDescriptor && !existingDescriptor.get && !existingDescriptor.set ? existingDescriptor.value : undefined; Object.defineProperty(target as any, propertyKey, { get() { if (existingDescriptor?.get) { return existingDescriptor.get.call(this); } if ( !Object.prototype.hasOwnProperty.call(this, internalValueKey) && initialValue !== undefined ) { (this as any)[internalValueKey] = initialValue; } return (this as any)[internalValueKey]; }, set(newValue: unknown) { if (existingDescriptor?.set) { existingDescriptor.set.call(this, newValue); } else { (this as any)[internalValueKey] = newValue; } if ( !(this as any)[reflectUpdateFlagKey] && (this as any)[publisherKey] ) { (this as any)[publisherKey].set(newValue); } }, enumerable: existingDescriptor?.enumerable ?? true, configurable: existingDescriptor?.configurable ?? true, }); } (target as ConnectedComponent).__onConnected__((component) => { const state = (component as any)[stateKey] || ((component as any)[stateKey] = { cleanupWatchers: [] as Array<() => void>, unsubscribePublisher: null as null | (() => void), currentPath: null as string | null, }); if (state.unsubscribePublisher) { state.unsubscribePublisher(); state.unsubscribePublisher = null; } state.cleanupWatchers.forEach((cleanup: () => void) => cleanup()); state.cleanupWatchers = []; state.currentPath = null; const subscribeToPath = (resolvedPath: string | null) => { if (!resolvedPath) { if (state.unsubscribePublisher) { state.unsubscribePublisher(); state.unsubscribePublisher = null; } state.currentPath = null; (component as any)[publisherKey] = null; return; } if (resolvedPath === state.currentPath) { return; } if (state.unsubscribePublisher) { state.unsubscribePublisher(); state.unsubscribePublisher = null; } const publisher = getPublisherFromPath(resolvedPath); if (!publisher) { state.currentPath = null; (component as any)[publisherKey] = null; return; } const onAssign = (value: unknown) => { if (reflect && isUpdatingFromPublisherKey) { (component as any)[isUpdatingFromPublisherKey] = true; } component[propertyKey] = value; if (reflect && isUpdatingFromPublisherKey) { (component as any)[isUpdatingFromPublisherKey] = false; } }; publisher.onAssign(onAssign); state.unsubscribePublisher = () => { publisher.offAssign(onAssign); if ((component as any)[publisherKey] === publisher) { (component as any)[publisherKey] = null; } }; state.currentPath = resolvedPath; (component as any)[publisherKey] = publisher; }; const refreshSubscription = () => { if (isDynamicPath) { const resolution = resolveDynamicPath(component, path); if (!resolution.ready) { subscribeToPath(null); return; } subscribeToPath(resolution.path); return; } subscribeToPath(path); }; if (isDynamicPath) { for (const dependency of dynamicDependencies) { const unsubscribe = registerDynamicWatcher( component as Record, dependency, () => refreshSubscription() ); state.cleanupWatchers.push(unsubscribe); } } refreshSubscription(); }); (target as ConnectedComponent).__onDisconnected__((component) => { const state = (component as any)[stateKey]; if (!state) return; if (state.unsubscribePublisher) { state.unsubscribePublisher(); state.unsubscribePublisher = null; } state.cleanupWatchers.forEach((cleanup: () => void) => cleanup()); state.cleanupWatchers = []; state.currentPath = null; (component as any)[publisherKey] = null; }); }; }