import type { DataProviderKey, DataProviderKeyHost, } from "../utils/dataProviderKey"; import { Endpoint } from "../utils/endpoint"; import HTML, { SearchableDomElement, } from "@supersoniks/concorde/core/utils/HTML"; import API, { APIConfiguration, type ApiGetResult, } from "@supersoniks/concorde/core/utils/api"; import DataProvider from "../utils/PublisherProxy"; import { ConnectedComponent, setSubscribable } from "./subscriber/common"; import { extractDynamicDependencies, resolveDynamicPath, } from "./subscriber/dynamicPath"; import { getDynamicWatchKeys, registerDynamicPropertyWatcher, } from "./subscriber/dynamicPropertyWatch"; import { getPublisherFromPath } from "./subscriber/publisherPath"; function asSearchableHost(component: unknown): SearchableDomElement | null { if (component instanceof HTMLElement || component instanceof ShadowRoot) { return component; } return null; } function readApiConfigurationFromPublisher( publisher: DataProvider | null, ): APIConfiguration | null { if (!publisher || typeof publisher.get !== "function") return null; const raw = publisher.get(); if (!raw || typeof raw !== "object") return null; if (!("serviceURL" in (raw as object))) return null; return raw as APIConfiguration; } function resolveScopedConfiguration( component: unknown, ): APIConfiguration | null { const host = asSearchableHost(component); if (!host) return null; return HTML.getApiConfiguration(host); } type ApiGetState = { cleanupWatchers: Array<() => void>; requestGeneration: number; configPublisher: DataProvider | null; configMutationHandler: (() => void) | null; }; function detachConfigPublisher(state: ApiGetState): void { if (state.configPublisher && state.configMutationHandler) { state.configPublisher.offInternalMutation(state.configMutationHandler); } state.configPublisher = null; state.configMutationHandler = null; } /** * Décorateur **`@get`** : charge des données via `API.getDetailed` et assigne un * `ApiGetResult` (`request`, `response`, `result` typé `T`) ou `null`. * Le path est un `Endpoint` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte). * * **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté * (`HTMLElement` / `ShadowRoot`). Sans hôte DOM valide, la propriété reste inchangée jusqu’à connexion. * * **Deuxième paramètre** : `DataProviderKey` — la config est lue via * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`). * Toute mutation interne du publisher (`onInternalMutation`) relance le GET. * * @example * @get(new Endpoint("users/${userId}")) * payload?: ApiGetResult; * * @example * const apiConf = new DataProviderKey("myApiConf"); * PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... }); * @get(new Endpoint("things/${id}"), apiConf) * payload?: ApiGetResult; */ export function get( endpoint: Endpoint, ): ( target: DataProviderKeyHost & { [P in K]?: ApiGetResult | null | undefined; }, propertyKey: K, ) => void; export function get( endpoint: Endpoint, configurationKey: DataProviderKey, ): ( target: DataProviderKeyHost & DataProviderKeyHost & { [P in K]?: ApiGetResult | null | undefined; }, propertyKey: K, ) => void; export function get( endpoint: Endpoint, configurationKey?: DataProviderKey, ): ( target: object & { [P in K]?: ApiGetResult | null | undefined }, propertyKey: K, ) => void { const pathTemplate = endpoint.path; const configurationKeyPath = configurationKey?.path; const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate); const configKeyDynamicDependencies = configurationKeyPath ? extractDynamicDependencies(configurationKeyPath) : []; const mergedDynamicDependencies = [ ...new Set([ ...endpointDynamicDependencies, ...configKeyDynamicDependencies, ]), ]; const isDynamicPath = endpointDynamicDependencies.length > 0; const usesPublisherConfig = Boolean(configurationKeyPath); return function (target: object, propertyKey: string) { if (!target) return; setSubscribable(target); const stateKey = `__get_state_${propertyKey}`; (target as ConnectedComponent).__onConnected__((component) => { const comp = component as Record; let state = comp[stateKey] as ApiGetState | undefined; if (!state) { state = { cleanupWatchers: [], requestGeneration: 0, configPublisher: null, configMutationHandler: null, }; comp[stateKey] = state; } state.cleanupWatchers.forEach((cleanup) => cleanup()); state.cleanupWatchers = []; state.requestGeneration++; const runFetch = () => { const resolution = isDynamicPath ? resolveDynamicPath(component, pathTemplate) : { ready: true, path: pathTemplate }; if (!resolution.ready || !resolution.path) { comp[propertyKey] = undefined; return; } let config: APIConfiguration | null = null; if (usesPublisherConfig && configurationKeyPath) { const configRes = resolveDynamicPath(component, configurationKeyPath); if (!configRes.ready || !configRes.path) { comp[propertyKey] = undefined; return; } const configPublisher = getPublisherFromPath(configRes.path); config = readApiConfigurationFromPublisher(configPublisher); } else { config = resolveScopedConfiguration(component); } if (!config) { comp[propertyKey] = undefined; return; } const generation = ++state.requestGeneration; const api = new API(config); void api .getDetailed(resolution.path) .then((payload?: ApiGetResult) => { if (generation !== state.requestGeneration) return; comp[propertyKey] = payload; }); }; const rebindPublisherConfig = () => { if (!usesPublisherConfig || !configurationKeyPath) return; detachConfigPublisher(state); const configRes = resolveDynamicPath(component, configurationKeyPath); if (!configRes.ready || !configRes.path) { comp[propertyKey] = undefined; return; } const publisher = getPublisherFromPath(configRes.path); if (!publisher) { comp[propertyKey] = undefined; return; } const mutationHandler = () => { runFetch(); }; publisher.onInternalMutation(mutationHandler); state.configPublisher = publisher; state.configMutationHandler = mutationHandler; }; if (usesPublisherConfig) { for (const dependency of mergedDynamicDependencies) { const unsubscribe = registerDynamicPropertyWatcher( getDynamicWatchKeys.watcherStore, getDynamicWatchKeys.hooked, component, dependency, () => rebindPublisherConfig(), ); state.cleanupWatchers.push(unsubscribe); } rebindPublisherConfig(); } else { if (isDynamicPath) { for (const dependency of endpointDynamicDependencies) { const unsubscribe = registerDynamicPropertyWatcher( getDynamicWatchKeys.watcherStore, getDynamicWatchKeys.hooked, component, dependency, () => runFetch(), ); state.cleanupWatchers.push(unsubscribe); } } runFetch(); } }); (target as ConnectedComponent).__onDisconnected__((component) => { const comp = component as Record; const state = comp[stateKey] as ApiGetState | undefined; if (!state) return; detachConfigPublisher(state); state.cleanupWatchers.forEach((cleanup) => cleanup()); state.cleanupWatchers = []; state.requestGeneration++; comp[propertyKey] = undefined; }); }; }