/*! * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ import { LazyPromise } from "@fluidframework/core-utils/internal"; import { IFluidDependencySynthesizer } from "./IFluidDependencySynthesizer.js"; import type { AsyncFluidObjectProvider, AsyncOptionalFluidObjectProvider, AsyncRequiredFluidObjectProvider, FluidObjectProvider, FluidObjectSymbolProvider, } from "./types.js"; /** * DependencyContainer is similar to a IoC Container. It takes providers and will * synthesize an object based on them when requested. * @legacy * @beta */ export class DependencyContainer implements IFluidDependencySynthesizer { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: use a real type here private readonly providers = new Map>(); private readonly parents: IFluidDependencySynthesizer[]; public get IFluidDependencySynthesizer(): this { return this; } public constructor(...parents: (IFluidDependencySynthesizer | undefined)[]) { this.parents = parents.filter((v): v is IFluidDependencySynthesizer => v !== undefined); } /** * Add a new provider * @param type - Name of the Type T being provided * @param provider - A provider that will resolve the T correctly when asked * @throws If passing a type that's already registered */ public register( type: T, provider: FluidObjectProvider>, ): void { if (this.providers.has(type)) { throw new Error( `Attempting to register a provider of type ${String(type)} that already exists`, ); } this.providers.set(type, provider); } /** * Remove a provider * @param type - Name of the provider to remove */ public unregister(type: keyof TMap): void { if (this.providers.has(type)) { this.providers.delete(type); } } /** * {@inheritDoc (IFluidDependencySynthesizer:interface).synthesize} */ public synthesize>( optionalTypes: FluidObjectSymbolProvider, requiredTypes: Required>, ): AsyncFluidObjectProvider { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const base: AsyncFluidObjectProvider = {} as AsyncFluidObjectProvider; this.generateRequired(base, requiredTypes); this.generateOptional(base, optionalTypes); Object.defineProperty(base, IFluidDependencySynthesizer, { get: () => this }); return base; } /** * {@inheritDoc (IFluidDependencySynthesizer:interface).has} * @param excludeParents - If true, exclude checking parent registries */ public has(type: string, excludeParents?: boolean): boolean { if (this.providers.has(type as keyof TMap)) { return true; } if (excludeParents !== true) { return this.parents.some((p: IFluidDependencySynthesizer) => p.has(type)); } return false; } /** * Get a provider for the given type. * @param provider - The name of the provider to get * @deprecated Needed for backwards compatability. */ private getProvider(provider: string & keyof TMap): PropertyDescriptor | undefined { // this was removed, but some partners have trouble with back compat where they // use invalid patterns with FluidObject and IFluidDependencySynthesizer // this is just for back compat until those are removed if (this.has(provider)) { if (this.providers.has(provider)) { return this.providers.get(provider); } for (const parent of this.parents) { if (parent instanceof DependencyContainer) { return parent.getProvider(provider); } else { // older implementations of the IFluidDependencySynthesizer exposed getProvider const maybeGetProvider = parent as { getProvider?(provider: string & keyof TMap) }; if (maybeGetProvider?.getProvider !== undefined) { return maybeGetProvider.getProvider(provider); } } } } } private generateRequired( base: AsyncRequiredFluidObjectProvider, types: Required>, ): void { if (types === undefined) { return; } for (const key of Object.keys(types) as unknown as (keyof TMap)[]) { const provider = this.resolveProvider(key); if (provider === undefined) { throw new Error( `Object attempted to be created without registered required provider ${String(key)}`, ); } Object.defineProperty(base, key, provider); } } private generateOptional( base: AsyncOptionalFluidObjectProvider, types: FluidObjectSymbolProvider, ): void { if (types === undefined) { return; } for (const key of Object.keys(types) as unknown as (keyof TMap)[]) { // back-compat: in 0.56 we allow undefined in the types, but we didn't before // this will keep runtime back compat, eventually we should support undefined properties // rather than properties that return promises that resolve to undefined const provider = this.resolveProvider(key) ?? { get: async () => undefined }; Object.defineProperty(base, key, provider); } } private resolveProvider(t: T): PropertyDescriptor | undefined { // If we have the provider return it // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const provider = this.providers.get(t); if (provider === undefined) { for (const parent of this.parents) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const sp = { [t]: t } as FluidObjectSymbolProvider>; const syn = parent.synthesize, Record>(sp, {}); const descriptor = Object.getOwnPropertyDescriptor(syn, t); if (descriptor !== undefined) { return descriptor; } } return undefined; } // The double nested gets are required for lazy loading the provider resolution if (typeof provider === "function") { return { // eslint-disable-next-line @typescript-eslint/promise-function-async get() { if (provider && typeof provider === "function") { return ( Promise.resolve(this[IFluidDependencySynthesizer]) // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call .then(async (fds): Promise => provider(fds)) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access .then((p) => p?.[t]) ); } }, }; } return { get() { if (provider) { return new LazyPromise(async () => { return Promise.resolve(provider).then((p) => { if (p) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return p[t]; } }); }); } }, }; } }