// // Copyright 2025 DXOS.org // import { type Registry, Rx } from '@effect-rx/rx-react'; import { Effect } from 'effect'; import { Trigger } from '@dxos/async'; import { invariant } from '@dxos/invariant'; import { log } from '@dxos/log'; import { type MaybePromise } from '@dxos/util'; import { type ActivationEvent } from './events'; const InterfaceDefTypeId: unique symbol = Symbol.for('InterfaceDefTypeId'); /** * The interface definition of a capability. */ export type InterfaceDef = { [InterfaceDefTypeId]: T; identifier: string; }; /** * Helper to define the interface of a capability. */ export const defineCapability = (identifier: string) => { return { identifier } as InterfaceDef; }; /** * A unique string identifier with a Typescript type associated with it. * When a capability is contributed to the application an implementation of the interface is provided. */ export type Capability = { /** * The interface definition of the capability. */ interface: InterfaceDef; /** * The implementation of the capability. */ implementation: T; /** * Called when the capability is deactivated. */ deactivate?: () => MaybePromise | Effect.Effect; }; export type AnyCapability = Capability; type PluginsContextOptions = { registry: Registry.Registry; activate: (event: ActivationEvent) => Effect.Effect; reset: (event: ActivationEvent) => Effect.Effect; }; // NOTE: This is implemented as a class to prevent it from being proxied by PluginManager state. class CapabilityImpl { constructor( readonly moduleId: string, readonly implementation: T, ) {} } /** * Helper to define the implementation of a capability. */ export const contributes = ( interfaceDef: Capability['interface'], implementation: Capability['implementation'], deactivate?: Capability['deactivate'], ): Capability => { return { interface: interfaceDef, implementation, deactivate } satisfies Capability; }; type LoadCapability = () => Promise<{ default: (props: T) => MaybePromise> }>; type LoadCapabilities = () => Promise<{ default: (props: T) => MaybePromise }>; // TODO(wittjosiah): Not having the array be `any` causes type errors when using the lazy capability. type LazyCapability = (props?: T) => Promise<() => Promise | AnyCapability[]>>; /** * Helper to define a lazily loaded implementation of a capability. */ export const lazy = (c: LoadCapability | LoadCapabilities): LazyCapability => async (props?: T) => { const { default: getCapability } = await c(); return async () => getCapability(props as T); }; /** * Facilitates the dependency injection between [plugin modules](#pluginmodule) by allowing them contribute and request capabilities from each other. * It tracks the capabilities that are contributed in an in-memory live object. * This allows the application to subscribe to this state and incorporate plugins which are added dynamically. */ export class PluginContext { private readonly _registry: Registry.Registry; private readonly _capabilityImpls = Rx.family[]>>(() => { return Rx.make[]>([]).pipe(Rx.keepAlive); }); readonly _capabilities = Rx.family>((id: string) => { return Rx.make((get) => { const current = get(this._capabilityImpls(id)); return current.map((c) => c.implementation); }); }); readonly _capability = Rx.family>((id: string) => { return Rx.make((get) => { const current = get(this._capabilities(id)); invariant(current.length > 0, `No capability found for ${id}`); return current[0]; }); }); /** * Activates plugins based on the activation event. * @param event The activation event. * @returns Whether the activation was successful. */ readonly activate: PluginsContextOptions['activate']; /** * Re-activates the modules that were activated by the event. * @param event The activation event. * @returns Whether the reset was successful. */ readonly reset: PluginsContextOptions['reset']; constructor({ registry, activate, reset }: PluginsContextOptions) { this._registry = registry; this.activate = activate; this.reset = reset; } /** * @internal */ contributeCapability({ module: moduleId, interface: interfaceDef, implementation, }: { module: string; interface: InterfaceDef; implementation: T; }): void { const current = this._registry.get(this._capabilityImpls(interfaceDef.identifier)); const capability = new CapabilityImpl(moduleId, implementation); if (current.includes(capability)) { return; } this._registry.set(this._capabilityImpls(interfaceDef.identifier), [...current, capability]); log('capability contributed', { id: interfaceDef.identifier, moduleId, count: current.length, }); } /** * @internal */ removeCapability(interfaceDef: InterfaceDef, implementation: T): void { const current = this._registry.get(this._capabilityImpls(interfaceDef.identifier)); if (current.length === 0) { return; } const next = current.filter((c) => c.implementation !== implementation); if (next.length !== current.length) { this._registry.set(this._capabilityImpls(interfaceDef.identifier), next); log('capability removed', { id: interfaceDef.identifier, count: current.length }); } else { log.warn('capability not removed', { id: interfaceDef.identifier }); } } /** * Get the Rx reference to the available capabilities for a given interface. * Primarily useful for deriving other Rx values based on the capabilities or * for subscribing to changes in the capabilities. * @returns An Rx reference to the available capabilities. */ capabilities(interfaceDef: InterfaceDef): Rx.Rx { // NOTE: This the type-checking for capabilities is done at the time of contribution. return this._capabilities(interfaceDef.identifier) as Rx.Rx; } /** * Get the Rx reference to the available capabilities for a given interface. * Primarily useful for deriving other Rx values based on the capability or * for subscribing to changes in the capability. * @returns An Rx reference to the available capability. * @throws If no capability is found. */ capability(interfaceDef: InterfaceDef): Rx.Rx { // NOTE: This the type-checking for capabilities is done at the time of contribution. return this._capability(interfaceDef.identifier) as Rx.Rx; } /** * Get capabilities from the plugin context. * @returns An array of capabilities. */ getCapabilities(interfaceDef: InterfaceDef): T[] { return this._registry.get(this.capabilities(interfaceDef)); } /** * Requests a single capability from the plugin context. * @returns The capability. * @throws If no capability is found. */ getCapability(interfaceDef: InterfaceDef): T { return this._registry.get(this.capability(interfaceDef)); } /** * Waits for a capability to be available. * @returns The capability. */ async waitForCapability(interfaceDef: InterfaceDef): Promise { const [capability] = this.getCapabilities(interfaceDef); if (capability) { return capability; } const trigger = new Trigger(); const cancel = this._registry.subscribe(this.capabilities(interfaceDef), (capabilities) => { if (capabilities.length > 0) { trigger.wake(capabilities[0]); } }); const result = await trigger.wait(); cancel(); return result; } async activatePromise(event: ActivationEvent): Promise { return this.activate(event).pipe(Effect.runPromise); } async resetPromise(event: ActivationEvent): Promise { return this.reset(event).pipe(Effect.runPromise); } }