import type { ComponentType } from "react"; import { objectEntries } from "@homarr/common"; import type { WidgetKind } from "@homarr/definitions"; import { widgetKinds as declaredWidgetKinds } from "@homarr/definitions"; import type { SettingsContextProps } from "@homarr/settings/creator"; import type { WidgetComponentProps, WidgetDefinition } from "./definition"; import type { WidgetOptionDefinition } from "./options"; import type { WidgetImports } from "./registry"; import { widgetComponentLoaders, widgetModuleLoaders } from "./registry"; type RegisteredWidgetModule = WidgetImports[WidgetKind]; type RegisteredWidgetComponentModule = Awaited>; type WidgetDefinitionFor = WidgetImports[TKind]["definition"] & WidgetDefinition; type WidgetComponentModule = { default: ComponentType>; }; export type WidgetModule = { definition: WidgetDefinitionFor; componentLoader: () => Promise>; }; export type WidgetResources = { definition: WidgetDefinitionFor; Component: ComponentType>; }; type RegisteredWidgetResources = { definition: RegisteredWidgetModule["definition"]; Component: RegisteredWidgetComponentModule["default"]; }; type WidgetManifestValue = { module: WidgetModule; component: WidgetComponentModule; resources: WidgetResources; definition: WidgetDefinitionFor; }; type RegisteredWidgetManifestValue = { module: RegisteredWidgetModule; component: RegisteredWidgetComponentModule; resources: RegisteredWidgetResources; definition: WidgetDefinition; }; const definitionPromises = new Map>(); export const widgetKinds = [...declaredWidgetKinds]; export const createRetryableLoader = (loaders: ReadonlyMap Promise>) => { const promises = new Map>(); return (key: TKey) => { const existing = promises.get(key); if (existing) return existing; const loader = loaders.get(key); if (!loader) throw new Error(`No loader is registered for ${String(key)}`); const promise = loader(); promises.set(key, promise); void promise.catch(() => { if (promises.get(key) === promise) promises.delete(key); }); return promise; }; }; const loadRegisteredWidgetModule = createRetryableLoader( new Map Promise>(objectEntries(widgetModuleLoaders)), ); // ponytail: TypeScript cannot correlate a generic key with the matching value // in a Map. The registry itself is checked against WidgetImports, so this // assertion only restores that already-verified relationship after caching. const correlateWidgetPromise = >( _kind: TKind, _type: TType, promise: Promise, ) => promise as Promise[TType]>; export const loadWidgetModule = (kind: TKind) => correlateWidgetPromise(kind, "module", loadRegisteredWidgetModule(kind)); const loadRegisteredWidgetComponent = createRetryableLoader( new Map Promise>(objectEntries(widgetComponentLoaders)), ); export const loadWidgetComponent = (kind: TKind) => correlateWidgetPromise(kind, "component", loadRegisteredWidgetComponent(kind)); const loadRegisteredWidgetResources = createRetryableLoader( new Map Promise>( widgetKinds.map((kind) => [ kind, async () => { const [{ definition }, { default: Component }] = await Promise.all([ loadRegisteredWidgetModule(kind), loadRegisteredWidgetComponent(kind), ]); return { definition, Component }; }, ]), ), ); export const loadWidgetResources = (kind: TKind) => correlateWidgetPromise(kind, "resources", loadRegisteredWidgetResources(kind)); export const loadWidgetDefinition = (kind: TKind) => { const existing = definitionPromises.get(kind); if (existing) { return correlateWidgetPromise(kind, "definition", existing); } const promise = loadRegisteredWidgetModule(kind).then(({ definition }) => definition); definitionPromises.set(kind, promise); void promise.catch(() => { if (definitionPromises.get(kind) === promise) definitionPromises.delete(kind); }); return correlateWidgetPromise(kind, "definition", promise); }; let allWidgetDefinitionsPromise: Promise> | undefined; export const loadAllWidgetDefinitions = () => { if (allWidgetDefinitionsPromise) return allWidgetDefinitionsPromise; const promise = Promise.all(widgetKinds.map(async (kind) => [kind, await loadWidgetDefinition(kind)] as const)).then( (entries) => new Map(entries), ); allWidgetDefinitionsPromise = promise; void promise.catch(() => { if (allWidgetDefinitionsPromise === promise) allWidgetDefinitionsPromise = undefined; }); return promise; }; export const reduceWidgetOptionsWithDefinition = ( definition: WidgetDefinition, settings: Pick, currentValue: Record = {}, ) => { const options: Record = definition.createOptions(settings); const result: Record = {}; for (const [key, value] of objectEntries(options)) { result[key] = currentValue[key] ?? value.defaultValue; } return result; };