packages/modules/packages/module/packages/resolver/src/CompositeModuleResolver.ts
/* eslint-disable max-statements */
import { assertEx } from '@xylabs/assert'
import { exists } from '@xylabs/exists'
import { Address } from '@xylabs/hex'
import { Promisable } from '@xylabs/promise'
import {
CacheConfig,
duplicateModules,
ModuleFilter,
ModuleFilterOptions,
ModuleIdentifier,
ModuleIdentifierPart,
ModuleIdentifierTransformer,
ModuleInstance,
ModuleRepository,
ModuleResolverInstance,
ObjectFilterOptions,
ObjectResolverPriority,
ResolveHelper,
} from '@xyo-network/module-model'
import { LRUCache } from 'lru-cache'
import { AbstractModuleResolver, ModuleResolverParams } from './AbstractModuleResolver'
import { SimpleModuleResolver } from './SimpleModuleResolver'
export interface CompositeModuleResolverParams extends ModuleResolverParams {
allowNameResolution?: boolean
cache?: CacheConfig
moduleIdentifierTransformers?: ModuleIdentifierTransformer[]
}
const moduleIdentifierParts = (moduleIdentifier: ModuleIdentifier): ModuleIdentifierPart[] => {
return moduleIdentifier?.split(':') as ModuleIdentifierPart[]
}
export class CompositeModuleResolver<T extends CompositeModuleResolverParams = CompositeModuleResolverParams>
extends AbstractModuleResolver<T>
implements ModuleRepository, ModuleResolverInstance
{
static defaultMaxDepth = 3
protected _cache: LRUCache<ModuleIdentifier, ModuleInstance>
protected resolvers: ModuleResolverInstance[] = []
private _allowAddResolver = true
private _localResolver: SimpleModuleResolver
constructor(params: T) {
super(params)
const localResolver = new SimpleModuleResolver({ allowNameResolution: params.allowNameResolution, root: params.root })
this.addResolver(localResolver)
const { max = 100, ttl = 1000 * 5 /* five seconds */ } = params.cache ?? {}
this._cache = new LRUCache<ModuleIdentifier, ModuleInstance>({ max, ttl, ...params.cache })
this._localResolver = localResolver
}
get allowAddResolver() {
return this._allowAddResolver
}
set allowAddResolver(value: boolean) {
this.resolvers = [this._localResolver]
this._allowAddResolver = value
}
get allowNameResolution() {
return this.params.allowNameResolution ?? true
}
private get moduleIdentifierTransformers() {
return this.params.moduleIdentifierTransformers ?? ResolveHelper.transformers
}
add(module: ModuleInstance): this
add(module: ModuleInstance[]): this
add(module: ModuleInstance | ModuleInstance[]): this {
if (Array.isArray(module)) {
for (const mod of module) this.addSingleModule(mod)
} else {
this.addSingleModule(module)
}
return this
}
addResolver(resolver: ModuleResolverInstance): this {
if (this.allowAddResolver) {
this.resolvers.push(resolver)
}
return this
}
remove(addresses: Address[] | Address): this {
if (Array.isArray(addresses)) {
for (const address of addresses) this.removeSingleModule(address)
} else {
this.removeSingleModule(addresses)
}
return this
}
removeResolver(resolver: ModuleResolverInstance): this {
this.resolvers = this.resolvers.filter((item) => item !== resolver)
return this
}
// eslint-disable-next-line complexity
async resolveHandler<T extends ModuleInstance = ModuleInstance>(
idOrFilter: ModuleFilter<T> | ModuleIdentifier = '*',
options: ModuleFilterOptions<T> = {},
): Promise<T[]> {
const mutatedOptions = { ...options, maxDepth: options?.maxDepth ?? CompositeModuleResolver.defaultMaxDepth }
//resolve all
if (idOrFilter === '*') {
const all = idOrFilter
//wen't too far?
if (mutatedOptions.maxDepth < 0) {
return []
}
//identity resolve?
if (mutatedOptions.maxDepth === 0) {
return (await this._localResolver.resolve(all, mutatedOptions)) ?? []
}
const childOptions = { ...mutatedOptions, maxDepth: mutatedOptions?.maxDepth - 1 }
const result = await Promise.all(
this.resolvers.map(async (resolver) => {
const result: T[] = await resolver.resolve<T>(all, childOptions)
return result
}),
)
const flatResult: T[] = result.flat().filter(exists)
return flatResult.filter(duplicateModules)
}
if (typeof idOrFilter === 'string') {
//wen't too far?
if (mutatedOptions.maxDepth < 0) {
return []
}
//resolve ModuleIdentifier
const idParts = moduleIdentifierParts(idOrFilter)
if (idParts.length > 1) {
const mod = await this.resolveMultipartIdentifier<T>(idOrFilter)
return (
mod ?
Array.isArray(mod) ?
mod
: [mod]
: []
)
}
const id = await ResolveHelper.transformModuleIdentifier(idOrFilter, this.moduleIdentifierTransformers)
if (id) {
if (mutatedOptions.maxDepth < 0) {
return []
}
const cachedResult = this._cache.get(id)
if (cachedResult) {
if (cachedResult.status === 'dead') {
this._cache.delete(id)
} else {
return [cachedResult] as T[]
}
}
//identity resolve?
if (mutatedOptions.maxDepth === 0) {
const mod = await this._localResolver.resolve(idOrFilter, mutatedOptions)
return (
mod ?
Array.isArray(mod) ?
mod
: [mod]
: []
)
}
//recursive function to resolve by priority
const resolvePriority = async (priority: ObjectResolverPriority): Promise<T | undefined> => {
const resolvers = this.resolvers.filter((resolver) => resolver.priority === priority)
const results: T[] = (
await Promise.all(
resolvers.map(async (resolver) => {
const result: T | undefined = await resolver.resolve<T>(id, mutatedOptions)
return result
}),
)
).filter(exists)
const result: T | undefined = results.filter(exists).filter(duplicateModules).pop()
if (result) {
this._cache.set(id, result)
return result
}
return priority === ObjectResolverPriority.VeryLow ? undefined : await resolvePriority(priority - 1)
}
const mod = await resolvePriority(ObjectResolverPriority.VeryHigh)
return (
mod ?
Array.isArray(mod) ?
mod
: [mod]
: []
)
}
} else if (typeof idOrFilter === 'object') {
//wen't too far?
if (mutatedOptions.maxDepth < 0) {
return []
}
const filter = idOrFilter
//identity resolve?
if (mutatedOptions.maxDepth === 0) {
return await this._localResolver.resolve(filter, mutatedOptions)
}
const childOptions = { ...mutatedOptions, maxDepth: mutatedOptions?.maxDepth - 1 }
const result = await Promise.all(
this.resolvers.map(async (resolver) => {
const result: T[] = await resolver.resolve<T>(filter, childOptions)
return result
}),
)
const flatResult: T[] = result.flat().filter(exists)
return flatResult.filter(duplicateModules)
}
return []
}
async resolveIdentifier(id: ModuleIdentifier, _options?: ObjectFilterOptions): Promise<Address | undefined> {
const idParts = id.split(':')
if (idParts.length > 1) {
return this.resolveComplexIdentifier(id)
}
const results = (
await Promise.all(
this.resolvers.map(async (resolver) => {
const result = await resolver.resolveIdentifier(id)
return result
}),
)
).filter(exists)
const result = results.shift()
if (results.length > 0) {
for (const altResult of results) {
assertEx(altResult === result, () => `Inconsistent results for ${id} [${result}][${altResult}]`)
}
}
return result
}
protected resolveComplexIdentifier(_id: ModuleIdentifier, _options?: ObjectFilterOptions): Promisable<Address | undefined> {
throw new Error('Method not implemented.')
}
private addSingleModule(module?: ModuleInstance) {
if (module) {
this._localResolver.add(module)
}
}
private removeSingleModule(address: Address) {
this._localResolver.remove(address)
}
private async resolveMultipartIdentifier<T extends ModuleInstance = ModuleInstance>(moduleIdentifier: ModuleIdentifier): Promise<T | undefined> {
const idParts = moduleIdentifierParts(moduleIdentifier)
assertEx(idParts.length >= 2, () => 'Not a valid multipart identifier')
const id = assertEx(idParts.shift())
const module = (await this.resolve<T>(id)) ?? (await this.resolvePrivate<T>(id))
return (await module?.resolve<T>(idParts.join(':'))) ?? (await module?.resolvePrivate<T>(idParts.join(':')))
}
}