diff --git a/packages/caching/src/Builder.ts b/packages/caching/src/Builder.ts new file mode 100644 index 00000000..7ce62f36 --- /dev/null +++ b/packages/caching/src/Builder.ts @@ -0,0 +1,32 @@ + +import { FileManager, Files } from '@jitar/runtime'; + +import { ApplicationReader, ApplicationBuilder } from './application'; + +export default class Builder +{ + #projectFileManager: FileManager; + #appFileManager: FileManager; + + #applicationReader: ApplicationReader; + #applicationBuilder: ApplicationBuilder; + + constructor(projectFileManager: FileManager, appFileManager: FileManager) + { + this.#projectFileManager = projectFileManager; + this.#appFileManager = appFileManager; + + this.#applicationReader = new ApplicationReader(appFileManager); + this.#applicationBuilder = new ApplicationBuilder(appFileManager); + } + + async build(): Promise + { + const moduleFiles = await this.#appFileManager.filter(Files.MODULE_PATTERN); + const segmentFiles = await this.#projectFileManager.filter(Files.SEGMENT_PATTERN); + + const application = await this.#applicationReader.read(moduleFiles, segmentFiles); + + return this.#applicationBuilder.build(application); + } +} diff --git a/packages/caching/src/application/Builder.ts b/packages/caching/src/application/Builder.ts new file mode 100644 index 00000000..c8d19636 --- /dev/null +++ b/packages/caching/src/application/Builder.ts @@ -0,0 +1,27 @@ + +import type { FileManager } from '@jitar/runtime'; + +import type Application from './models/Application'; + +import ModuleBuilder from './ModuleBuilder'; +import SegmentBuilder from './SegmentBuilder'; + +export default class Builder +{ + #moduleBuilder: ModuleBuilder; + #segmentBuilder: SegmentBuilder; + + constructor(fileManager: FileManager) + { + this.#moduleBuilder = new ModuleBuilder(fileManager); + this.#segmentBuilder = new SegmentBuilder(fileManager); + } + + async build(application: Application): Promise + { + await Promise.all([ + this.#moduleBuilder.build(application), + this.#segmentBuilder.build(application) + ]); + } +} diff --git a/packages/caching/src/application/ClassSourceBuilder.ts b/packages/caching/src/application/ClassSourceBuilder.ts new file mode 100644 index 00000000..5c00f8a2 --- /dev/null +++ b/packages/caching/src/application/ClassSourceBuilder.ts @@ -0,0 +1,22 @@ + +import { Module } from '../module'; + +export default class ClassSourceBuilder +{ + #module: Module; + + constructor(module: Module) + { + this.#module = module; + } + + build(): string + { + const filename = this.#module.filename; + const classes = this.#module.reflection.exportedClasses; + const classNames = classes.map(clazz => clazz.name); + const sourceCode = classNames.map(className => `${className}.source = "/${filename}";`); + + return sourceCode.join('\n'); + } +} diff --git a/packages/caching/src/application/ImportRewriter.ts b/packages/caching/src/application/ImportRewriter.ts new file mode 100644 index 00000000..2697045e --- /dev/null +++ b/packages/caching/src/application/ImportRewriter.ts @@ -0,0 +1,171 @@ + +import { ReflectionImport, Reflector } from '@jitar/reflection'; + +import { Module } from '../module'; +import { Segmentation, Segment } from '../segment'; +import { FileHelper } from '../utils'; + +const KEYWORD_DEFAULT = 'default'; +const IMPORT_PATTERN = /import\s(?:["'\s]*([\w*{}\n, ]+)from\s*)?["'\s]*([@\w/._-]+)["'\s].*/g; +const APPLICATION_MODULE_INDICATORS = ['.', '/', 'http:', 'https:']; + +const reflector = new Reflector(); + +export default class ImportRewriter +{ + #module: Module; + #segmentation: Segmentation; + #segment: Segment | undefined; + + constructor(module: Module, segmentation: Segmentation, segment?: Segment) + { + this.#module = module; + this.#segmentation = segmentation; + this.#segment = segment; + } + + rewrite(): string + { + const replacer = (statement: string) => this.#replaceImport(statement); + + const code = this.#module.code; + + return code.replaceAll(IMPORT_PATTERN, replacer); + } + + #replaceImport(statement: string): string + { + const dependency = reflector.parseImport(statement); + + return this.#isApplicationModule(dependency) + ? this.#rewriteApplicationImport(dependency) + : this.#rewriteRuntimeImport(dependency); + } + + #isApplicationModule(dependency: ReflectionImport): boolean + { + return APPLICATION_MODULE_INDICATORS.some(indicator => dependency.from.startsWith(indicator, 1)); + } + + #rewriteApplicationImport(dependency: ReflectionImport): string + { + const targetModuleFilename = this.#getTargetModuleFilename(dependency); + + if (this.#segmentation.isModuleSegmented(targetModuleFilename)) + { + // import segmented module + + if (this.#segment?.hasModule(targetModuleFilename)) + { + const from = this.#rewriteApplicationFrom(targetModuleFilename, this.#segment.name); + + return this.#rewriteToStaticImport(dependency, from); // same segment + } + + const from = this.#rewriteApplicationFrom(targetModuleFilename, 'remote'); + + return this.#rewriteToStaticImport(dependency, from); // different segments + } + + // import shared (unsegmented) module + + const from = this.#rewriteApplicationFrom(targetModuleFilename, 'shared'); + + return this.#segment === undefined + ? this.#rewriteToStaticImport(dependency, from) // shared to shared + : this.#rewriteToDynamicImport(dependency, from); // segmented to shared (prevent bundling) + } + + #rewriteRuntimeImport(dependency: ReflectionImport): string + { + const from = this.#rewriteRuntimeFrom(dependency); + + return this.#rewriteToStaticImport(dependency, from); + } + + #rewriteApplicationFrom(filename: string, scope: string): string + { + const callingModulePath = FileHelper.extractPath(this.#module.filename); + const relativeFilename = FileHelper.makePathRelative(filename, callingModulePath); + + return FileHelper.addSubExtension(relativeFilename, scope); + } + + #rewriteRuntimeFrom(dependency: ReflectionImport): string + { + return this.#stripFrom(dependency.from); + } + + #rewriteToStaticImport(dependency: ReflectionImport, from: string): string + { + if (dependency.members.length === 0) + { + return `import "${from}";`; + } + + const members = this.#rewriteStaticImportMembers(dependency); + + return `import ${members} from "${from}";`; + } + + #rewriteToDynamicImport(dependency: ReflectionImport, from: string): string + { + if (dependency.members.length === 0) + { + return `await import("${from}");`; + } + + const members = this.#rewriteDynamicImportMembers(dependency); + + return `const ${members} = await import("${from}");`; + } + + #rewriteStaticImportMembers(dependency: ReflectionImport): string + { + const defaultMember = dependency.members.find(member => member.name === KEYWORD_DEFAULT); + const hasDefaultMember = defaultMember !== undefined; + const defaultMemberImport = hasDefaultMember ? defaultMember.as : ''; + + const namedMembers = dependency.members.filter(member => member.name !== KEYWORD_DEFAULT); + const namedMemberImports = namedMembers.map(member => member.name !== member.as ? `${member.name} as ${member.as}` : member.name); + const hasNamedMembers = namedMemberImports.length > 0; + const groupedNamedMemberImports = hasNamedMembers ? `{ ${namedMemberImports.join(', ')} }` : ''; + + const separator = hasDefaultMember && hasNamedMembers ? ', ' : ''; + + return `${defaultMemberImport}${separator}${groupedNamedMemberImports}`; + } + + #rewriteDynamicImportMembers(dependency: ReflectionImport): string + { + if (this.#doesImportAll(dependency)) + { + return dependency.members[0].as; + } + + const members = dependency.members; + const memberImports = members.map(member => member.name !== member.as ? `${member.name} : ${member.as}` : member.name); + + return `{ ${memberImports.join(', ')} }`; + } + + #getTargetModuleFilename(dependency: ReflectionImport): string + { + const from = this.#stripFrom(dependency.from); + const callingModulePath = FileHelper.extractPath(this.#module.filename); + const translated = FileHelper.makePathAbsolute(from, callingModulePath); + + return FileHelper.assureExtension(translated); + } + + #doesImportAll(dependency: ReflectionImport): boolean + { + return dependency.members.length === 1 + && dependency.members[0].name === '*'; + } + + #stripFrom(from: string): string + { + return from.substring(1, from.length - 1); + } +} diff --git a/packages/caching/src/application/LocalModuleBuilder.ts b/packages/caching/src/application/LocalModuleBuilder.ts new file mode 100644 index 00000000..dd97ac12 --- /dev/null +++ b/packages/caching/src/application/LocalModuleBuilder.ts @@ -0,0 +1,20 @@ + +import { Module } from '../module'; +import { Segmentation, Segment } from '../segment'; + +import ClassSourceBuilder from './ClassSourceBuilder'; +import ImportRewriter from './ImportRewriter'; + +export default class LocalModuleBuilder +{ + build(module: Module, segmentation: Segmentation, segment?: Segment): string + { + const classSourceBuilder = new ClassSourceBuilder(module); + const importRewriter = new ImportRewriter(module, segmentation, segment); + + const importCode = importRewriter.rewrite(); + const sourceCode = classSourceBuilder.build(); + + return `${importCode}\n${sourceCode}`; + } +} diff --git a/packages/caching/src/application/ModuleBuilder.ts b/packages/caching/src/application/ModuleBuilder.ts new file mode 100644 index 00000000..08852706 --- /dev/null +++ b/packages/caching/src/application/ModuleBuilder.ts @@ -0,0 +1,88 @@ + +import type { FileManager } from '@jitar/runtime'; + +import { Module } from '../module'; +import { Segmentation, Segment } from '../segment'; +import { FileHelper } from '../utils'; + +import type Application from './models/Application'; + +import RemoteModuleBuilder from './RemoteModuleBuilder'; +import LocalModuleBuilder from './LocalModuleBuilder'; + +export default class ModuleBuilder +{ + #fileManager: FileManager; + #localModuleBuilder: LocalModuleBuilder; + #remoteModuleBuilder: RemoteModuleBuilder; + + constructor(fileManager: FileManager) + { + this.#fileManager = fileManager; + + this.#localModuleBuilder = new LocalModuleBuilder(); + this.#remoteModuleBuilder = new RemoteModuleBuilder(); + } + + async build(application: Application): Promise + { + const repository = application.repository; + const segmentation = application.segmentation; + + const builds = repository.modules.map(module => this.#buildModule(module, segmentation)); + + await Promise.all(builds); + } + + async #buildModule(module: Module, segmentation: Segmentation): Promise + { + const moduleSegments = segmentation.getSegments(module.filename); + + // If the module is not part of any segment, it is an application module + + if (moduleSegments.length === 0) + { + return this.#buildSharedModule(module, segmentation); + } + + // Otherwise, it is a segment module that can be called remotely + + const segmentBuilds = moduleSegments.map(segment => this.#buildSegmentModule(module, segment, segmentation)); + const remoteBuild = this.#buildRemoteModule(module, moduleSegments); + + await Promise.all([...segmentBuilds, remoteBuild]); + + this.#fileManager.delete(module.filename); + } + + async #buildSharedModule(module: Module, segmentation: Segmentation): Promise + { + const filename = FileHelper.addSubExtension(module.filename, 'shared'); + const code = this.#localModuleBuilder.build(module, segmentation); + + return this.#fileManager.write(filename, code); + } + + async #buildSegmentModule(module: Module, segment: Segment, segmentation: Segmentation): Promise + { + const filename = FileHelper.addSubExtension(module.filename, segment.name); + const code = this.#localModuleBuilder.build(module, segmentation, segment); + + return this.#fileManager.write(filename, code); + } + + async #buildRemoteModule(module: Module, segments: Segment[]): Promise + { + // The remote module contains calls to segmented procedures only + + const segmentModules = segments.map(segment => segment.getModule(module.filename)); + const implementations = segmentModules.flatMap(segmentModule => segmentModule!.implementations); + + // TODO: Make the list of implementations unique + + const filename = FileHelper.addSubExtension(module.filename, 'remote'); + const code = this.#remoteModuleBuilder.build(implementations); + + return this.#fileManager.write(filename, code); + } +} diff --git a/packages/caching/src/application/Reader.ts b/packages/caching/src/application/Reader.ts new file mode 100644 index 00000000..6a976b67 --- /dev/null +++ b/packages/caching/src/application/Reader.ts @@ -0,0 +1,28 @@ + +import type { FileManager } from '@jitar/runtime'; + +import { ModuleReader } from '../module'; +import { SegmentReader } from '../segment'; + +import Application from './models/Application.js'; + +export default class Reader +{ + #fileManager: FileManager; + + constructor(fileManager: FileManager) + { + this.#fileManager = fileManager; + } + + async read(moduleFiles: string[], segmentFiles: string[]): Promise + { + const moduleReader = new ModuleReader(this.#fileManager); + const repository = await moduleReader.readAll(moduleFiles); + + const segmentReader = new SegmentReader(this.#fileManager, repository); + const segmentation = await segmentReader.readAll(segmentFiles); + + return new Application(repository, segmentation); + } +} diff --git a/packages/caching/src/application/RemoteModuleBuilder.ts b/packages/caching/src/application/RemoteModuleBuilder.ts new file mode 100644 index 00000000..f21ac2c2 --- /dev/null +++ b/packages/caching/src/application/RemoteModuleBuilder.ts @@ -0,0 +1,127 @@ + +import { ReflectionDestructuredArray, ReflectionDestructuredObject, ReflectionDestructuredValue, ReflectionField, ReflectionParameter } from '@jitar/reflection'; +import { AccessLevels } from '@jitar/runtime'; + +import { SegmentImplementation as Implementation } from '../segment'; + +export default class RemoteModuleBuilder +{ + build(implementations: Implementation[]): string + { + let code = ''; + + for (const implementation of implementations) + { + code += implementation.access === AccessLevels.PRIVATE + ? this.#createPrivateCode(implementation) + : this.#createPublicCode(implementation); + } + + return code.trim(); + } + + #createPrivateCode(implementation: Implementation): string + { + // Private procedures are not accessible from the outside. + // Therefore we need to throw an error when they are called. + + const fqn = implementation.fqn; + const version = implementation.version; + + const declaration = this.#createDeclaration(implementation); + const body = `throw new ProcedureNotAccessible('${fqn}', '${version}');`; + + return this.#createFunction(declaration, body); + } + + #createPublicCode(implementation: Implementation): string + { + // Public procedures are accessible from the outside. + // Therefore we need to create a remote implementation. + + const fqn = implementation.fqn; + const version = implementation.version; + const args = this.#createArguments(implementation.executable.parameters); + + const declaration = this.#createDeclaration(implementation); + const body = `return __run('${fqn}', '${version}', { ${args} }, this);`; + + return this.#createFunction(declaration, body); + } + + #createParameters(parameters: ReflectionParameter[]): string + { + const result: string[] = []; + + for (const parameter of parameters) + { + if (parameter instanceof ReflectionField) + { + result.push(parameter.name); + } + else if (parameter instanceof ReflectionDestructuredArray) + { + result.push(parameter.toString()); + } + else if (parameter instanceof ReflectionDestructuredObject) + { + result.push(parameter.toString()); + } + } + + return result.join(', '); + } + + #createArguments(parameters: ReflectionParameter[]): string + { + const result = this.#extractArguments(parameters); + + return result.join(', '); + } + + #extractArguments(parameters: ReflectionParameter[]): string[] + { + const result: string[] = []; + + for (const parameter of parameters) + { + if (parameter instanceof ReflectionDestructuredValue) + { + const argumentz = this.#extractArguments(parameter.members); + + result.push(...argumentz); + } + else if (parameter instanceof ReflectionField) + { + const argument = this.#createNamedArgument(parameter); + + result.push(argument); + } + } + + return result; + } + + #createNamedArgument(parameter: ReflectionField): string + { + const key = parameter.name; + const value = key.startsWith('...') ? key.substring(3) : key; + + return `'${key}': ${value}`; + } + + #createDeclaration(implementation: Implementation): string + { + const name = implementation.executable.name; + const parameters = this.#createParameters(implementation.executable.parameters); + + const prefix = implementation.importDefault ? 'default ' : ''; + + return `\nexport ${prefix}async function ${name}(${parameters})`; + } + + #createFunction(declaration: string, body: string): string + { + return `${declaration} {\n\t${body}\n}\n`; + } +} diff --git a/packages/caching/src/application/SegmentBuilder.ts b/packages/caching/src/application/SegmentBuilder.ts new file mode 100644 index 00000000..a292ccc3 --- /dev/null +++ b/packages/caching/src/application/SegmentBuilder.ts @@ -0,0 +1,174 @@ + +import { type FileManager, VersionParser } from '@jitar/runtime'; +import { ReflectionDestructuredArray, ReflectionDestructuredObject, ReflectionField, ReflectionFunction, ReflectionParameter } from '@jitar/reflection'; + +import { Segment, SegmentModule } from '../segment'; +import { FileHelper } from '../utils'; + +import type Application from './models/Application'; + +const KEYWORD_DEFAULT = 'default'; +const RUNTIME_IMPORTS = 'import { Segment, Procedure, Implementation, Version, NamedParameter, ArrayParameter, ObjectParameter } from "jitar";'; + +export default class SegmentBuilder +{ + #fileManager: FileManager; + + constructor(fileManager: FileManager) + { + this.#fileManager = fileManager; + } + + async build(application: Application): Promise + { + const segmentation = application.segmentation; + + const builds = segmentation.segments.map(segment => this.#buildSegment(segment)); + + await Promise.all(builds); + } + + async #buildSegment(segment: Segment): Promise + { + const filename = `${segment.name}.segment.js`; + const code = this.#createCode(segment); + + await this.#fileManager.write(filename, code); + } + + #createCode(segment: Segment): string + { + const importCode = this.#createImportCode(segment); + const segmentCode = this.#createSegmentCode(segment); + + return `${importCode}\n${segmentCode}`; + } + + #createImportCode(segment: Segment): string + { + const moduleImports = this.#createModuleImports(segment); + + return `${RUNTIME_IMPORTS}\n${moduleImports}`; + } + + #createModuleImports(segment: Segment): string + { + const imports = []; + + for (const module of segment.modules) + { + const filename = FileHelper.addSubExtension(module.filename, segment.name); + const members = this.#createModuleImportMembers(module); + + const importRule = `import ${members} from "./${filename}";`; + + imports.push(importRule); + } + + return imports.join('\n'); + } + + #createModuleImportMembers(module: SegmentModule): string + { + const implementations = module.implementations; + + const defaultImplementation = implementations.find(implementation => implementation.importKey === KEYWORD_DEFAULT); + const hasDefaultImplementation = defaultImplementation !== undefined; + const defaultMemberImport = hasDefaultImplementation ? defaultImplementation.id : ''; + + const namedImplementations = implementations.filter(implementation => implementation.importKey !== KEYWORD_DEFAULT); + const nameImplementationImports = namedImplementations.map(implementation => `${implementation.importKey} as ${implementation.id}`); + const hasNamedImplementations = namedImplementations.length > 0; + const groupedNamedMemberImports = hasNamedImplementations ? `{ ${nameImplementationImports.join(', ')} }` : ''; + + const separator = hasDefaultImplementation && hasNamedImplementations ? ', ' : ''; + + return `${defaultMemberImport}${separator}${groupedNamedMemberImports}`; + } + + #createSegmentCode(segment: Segment): string + { + const lines: string[] = []; + + lines.push(`export const segment = new Segment("${segment.name}")`); + + for (const procedure of segment.procedures) + { + lines.push(`\t.addProcedure(new Procedure("${procedure.fqn}")`); + + for (const implementation of procedure.implementations) + { + const version = this.#createVersionCode(implementation.version); + const parameters = this.#createParametersCode(implementation.executable); + + lines.push(`\t\t.addImplementation(new Implementation(${version}, "${implementation.access}", ${parameters}, ${implementation.id}))`); + } + + lines.push('\t)'); + } + + return lines.join('\n'); + } + + #createVersionCode(versionString: string): string + { + const version = VersionParser.parse(versionString); + + return `new Version(${version.major}, ${version.minor}, ${version.patch})`; + } + + #createParametersCode(executable: ReflectionFunction): string + { + const result = this.#extractParameters(executable.parameters); + + return `[${result.join(', ')}]`; + } + + #extractParameters(parameters: ReflectionParameter[]): string[] + { + const result = []; + + // Named parameters are identified by their name. + // Destructured parameters are identified by their index. + + for (const parameter of parameters) + { + result.push(this.#extractParameter(parameter)); + } + + return result; + } + + #extractParameter(parameter: ReflectionParameter): string + { + if (parameter instanceof ReflectionDestructuredArray) + { + return this.#createArrayParameter(parameter); + } + else if (parameter instanceof ReflectionDestructuredObject) + { + return this.#createObjectParameter(parameter); + } + + return this.#createNamedParameter(parameter); + } + + #createNamedParameter(parameter: ReflectionField): string + { + return `new NamedParameter("${parameter.name}", ${parameter.value !== undefined})`; + } + + #createArrayParameter(parameter: ReflectionDestructuredArray): string + { + const members = this.#extractParameters(parameter.members); + + return `new ArrayParameter([${members.join(', ')}])`; + } + + #createObjectParameter(parameter: ReflectionDestructuredObject): string + { + const members = this.#extractParameters(parameter.members); + + return `new ObjectParameter([${members.join(', ')}])`; + } +} diff --git a/packages/caching/src/application/index.ts b/packages/caching/src/application/index.ts new file mode 100644 index 00000000..ae71b889 --- /dev/null +++ b/packages/caching/src/application/index.ts @@ -0,0 +1,3 @@ + +export { default as ApplicationReader } from './Reader'; +export { default as ApplicationBuilder } from './Builder'; diff --git a/packages/caching/src/application/models/Application.ts b/packages/caching/src/application/models/Application.ts new file mode 100644 index 00000000..7115d70e --- /dev/null +++ b/packages/caching/src/application/models/Application.ts @@ -0,0 +1,19 @@ + +import type { ModuleRepository } from '../../module'; +import type { Segmentation } from '../../segment'; + +export default class Application +{ + #repository: ModuleRepository; + #segmentation: Segmentation; + + constructor(repository: ModuleRepository, segmentation: Segmentation) + { + this.#repository = repository; + this.#segmentation = segmentation; + } + + get repository() { return this.#repository; } + + get segmentation() { return this.#segmentation; } +} diff --git a/packages/caching/src/lib.ts b/packages/caching/src/lib.ts index ceacc9bb..cd75ac3e 100644 --- a/packages/caching/src/lib.ts +++ b/packages/caching/src/lib.ts @@ -1,2 +1,2 @@ -export { default as CacheManager } from './CacheManager.js'; +export { default as CacheManager } from './Builder.js'; diff --git a/packages/caching/src/module/Reader.ts b/packages/caching/src/module/Reader.ts new file mode 100644 index 00000000..932d50f7 --- /dev/null +++ b/packages/caching/src/module/Reader.ts @@ -0,0 +1,52 @@ + +import { Reflector } from '@jitar/reflection'; +import type { FileManager } from '@jitar/runtime'; + +import FileNotLoaded from './errors/FileNotLoaded'; + +import Module from './models/Module'; +import Repository from './models/Repository'; + +const reflector = new Reflector(); + +export default class Reader +{ + #fileManager: FileManager; + + constructor(fileManager: FileManager) + { + this.#fileManager = fileManager; + } + + async readAll(filenames: string[]): Promise + { + const modules = await Promise.all(filenames.map(filename => this.read(filename))); + + return new Repository(modules); + } + + async read(filename: string): Promise + { + const relativeLocation = this.#fileManager.getRelativeLocation(filename); + const code = await this.#loadCode(filename); + const module = reflector.parse(code); + + return new Module(relativeLocation, code, module); + } + + async #loadCode(filename: string): Promise + { + try + { + const content = await this.#fileManager.getContent(filename); + + return content.toString(); + } + catch (error: unknown) + { + const message = error instanceof Error ? error.message : String(error); + + throw new FileNotLoaded(filename, message); + } + } +} diff --git a/packages/caching/src/module/errors/FileNotLoaded.ts b/packages/caching/src/module/errors/FileNotLoaded.ts new file mode 100644 index 00000000..e7b9091d --- /dev/null +++ b/packages/caching/src/module/errors/FileNotLoaded.ts @@ -0,0 +1,8 @@ + +export default class FileNotLoaded extends Error +{ + constructor(filename: string, message: string) + { + super(`Failed to load module file '${filename}' because of: ${message}`); + } +} diff --git a/packages/caching/src/module/index.ts b/packages/caching/src/module/index.ts new file mode 100644 index 00000000..21f96b8c --- /dev/null +++ b/packages/caching/src/module/index.ts @@ -0,0 +1,5 @@ + +export { default as Module } from './models/Module'; +export { default as ModuleRepository } from './models/Repository'; + +export { default as ModuleReader } from './Reader'; diff --git a/packages/caching/src/module/models/Module.ts b/packages/caching/src/module/models/Module.ts new file mode 100644 index 00000000..643c9d21 --- /dev/null +++ b/packages/caching/src/module/models/Module.ts @@ -0,0 +1,22 @@ + +import type { ReflectionModule } from '@jitar/reflection'; + +export default class Module +{ + #filename: string; + #code: string; + #reflection: ReflectionModule; + + constructor(filename: string, code: string, reflection: ReflectionModule) + { + this.#code = code; + this.#filename = filename; + this.#reflection = reflection; + } + + get filename() { return this.#filename; } + + get code() { return this.#code; } + + get reflection() { return this.#reflection; } +} diff --git a/packages/caching/src/module/models/Repository.ts b/packages/caching/src/module/models/Repository.ts new file mode 100644 index 00000000..fbecacfd --- /dev/null +++ b/packages/caching/src/module/models/Repository.ts @@ -0,0 +1,19 @@ + +import type Module from './Module'; + +export default class Repository +{ + #modules: Module[]; + + constructor(modules: Module[]) + { + this.#modules = modules; + } + + get modules() { return this.#modules; } + + get(filename: string): Module | undefined + { + return this.#modules.find(module => module.filename === filename); + } +} diff --git a/packages/caching/src/segment/Reader.ts b/packages/caching/src/segment/Reader.ts new file mode 100644 index 00000000..c6750e22 --- /dev/null +++ b/packages/caching/src/segment/Reader.ts @@ -0,0 +1,187 @@ + +import { ReflectionFunction } from '@jitar/reflection'; +import type { FileManager } from '@jitar/runtime'; + +import type { ModuleRepository } from '../module'; +import { FileHelper, IdGenerator } from '../utils'; + +import FunctionNotAsync from './errors/FunctionNotAsync'; +import InvalidFilename from './errors/InvalidFilename'; +import MissingModuleExport from './errors/MissingModuleExport'; +import FileNotLoaded from './errors/FileNotLoaded'; +import NotAFunction from './errors/NotAFunction'; +import ModuleNotFound from './errors/ModuleNotFound'; + +import Segmentation from './models/Segmentation'; +import Segment from './models/Segment'; +import Module from './models/Module'; +import Procedure from './models/Procedure'; +import Implementation from './models/Implementation'; + +import SegmentFile from './types/File'; + +const SEGMENT_FILE_EXTENSION = '.segment.json'; +const DEFAULT_ACCESS_LEVEL = 'private'; +const DEFAULT_VERSION_NUMBER = '0.0.0'; + +export default class SegmentReader +{ + #fileManager: FileManager; + #repository: ModuleRepository; + + constructor(fileManager: FileManager, repository: ModuleRepository) + { + this.#fileManager = fileManager; + this.#repository = repository; + } + + async readAll(filenames: string[]): Promise + { + const segments = await Promise.all(filenames.map(filename => this.read(filename))); + + return new Segmentation(segments); + } + + async read(filename: string): Promise + { + const definition = await this.#loadSegmentDefinition(filename); + + const name = this.#extractSegmentName(filename); + const modules = this.#createModules(definition); + const procedures = this.#createProcedures(modules); + + return new Segment(name, modules, procedures); + } + + #extractSegmentName(filename: string): string + { + const file = filename.split('/').pop(); + + if (file === undefined || file === '') + { + throw new InvalidFilename(filename); + } + + return file.replace(SEGMENT_FILE_EXTENSION, ''); + } + + async #loadSegmentDefinition(filename: string): Promise + { + try + { + const content = await this.#fileManager.getContent(filename); + + return JSON.parse(content.toString()) as SegmentFile; + } + catch (error: unknown) + { + const message = error instanceof Error ? error.message : String(error); + + throw new FileNotLoaded(filename, message); + } + } + + #createModules(definition: SegmentFile): Module[] + { + const modules: Module[] = []; + + for (const [filename, moduleImports] of Object.entries(definition)) + { + const moduleFilename = this.#makeModuleFilename(filename); + const location = this.#extractLocation(moduleFilename); + + const module = new Module(moduleFilename, location, moduleImports); + + modules.push(module); + } + + return modules; + } + + #makeModuleFilename(filename: string): string + { + const fullFilename = FileHelper.assureExtension(filename); + + if (fullFilename.startsWith('./')) return fullFilename.substring(2); + if (fullFilename.startsWith('/')) return fullFilename.substring(1); + + return fullFilename; + } + + #extractLocation(filename: string): string + { + const moduleParts = filename.split('/'); + moduleParts.pop(); + + return moduleParts.join('/'); + } + + #createProcedures(modules: Module[]): Procedure[] + { + const idGenerator = new IdGenerator(); + const procedures: Map = new Map(); + + for (const module of modules) + { + this.#extractModuleProcedures(module, procedures, idGenerator); + } + + return [...procedures.values()]; + } + + #extractModuleProcedures(module: Module, procedures: Map, idGenerator: IdGenerator): void + { + for (const [importKey, properties] of Object.entries(module.imports)) + { + // To make sure that we create the correct FQN, we need to get the executable + const executable = this.#getExecutable(module.filename, importKey); + + const procedureName = properties.as ?? executable.name; + const access = properties.access ?? DEFAULT_ACCESS_LEVEL; + const version = properties.version ?? DEFAULT_VERSION_NUMBER; + + const fqn = module.location !== '' ? `${module.location}/${procedureName}` : procedureName; + const isDefault = importKey === 'default'; + + const id = idGenerator.next(); + const implementation = new Implementation(id, importKey, fqn, access, version, isDefault, executable); + + module.addImplementation(implementation); + + const procedure = procedures.has(fqn) + ? procedures.get(fqn) as Procedure + : new Procedure(fqn); + + procedure.addImplementation(implementation); + + procedures.set(fqn, procedure); + } + } + + #getExecutable(filename: string, importKey: string): ReflectionFunction + { + const module = this.#repository.get(filename); + + if (module === undefined) + { + throw new ModuleNotFound(filename); + } + + const executable = module.reflection.getExported(importKey) as ReflectionFunction; + + if (executable === undefined) + { + throw new MissingModuleExport(filename, importKey); + } + else if ((executable instanceof ReflectionFunction) === false) + { + throw new NotAFunction(filename, importKey); + } + else if (executable.isAsync === false) + { + throw new FunctionNotAsync(filename, executable.name); + } + + return executable; + } +} diff --git a/packages/caching/src/segment/errors/DuplicateImplementation.ts b/packages/caching/src/segment/errors/DuplicateImplementation.ts new file mode 100644 index 00000000..e8350d9f --- /dev/null +++ b/packages/caching/src/segment/errors/DuplicateImplementation.ts @@ -0,0 +1,8 @@ + +export default class DuplicateImplementation extends Error +{ + constructor(fqn: string, version: string) + { + super(`Duplicate implementation found for '${fqn}' with version '${version}'.`); + } +} diff --git a/packages/caching/src/segment/errors/FileNotLoaded.ts b/packages/caching/src/segment/errors/FileNotLoaded.ts new file mode 100644 index 00000000..0ead9e57 --- /dev/null +++ b/packages/caching/src/segment/errors/FileNotLoaded.ts @@ -0,0 +1,8 @@ + +export default class FileNotLoaded extends Error +{ + constructor(filename: string, message: string) + { + super(`Failed to load segment file '${filename}' because of: ${message}`); + } +} diff --git a/packages/caching/src/segment/errors/FunctionNotAsync.ts b/packages/caching/src/segment/errors/FunctionNotAsync.ts new file mode 100644 index 00000000..0cb5a1b8 --- /dev/null +++ b/packages/caching/src/segment/errors/FunctionNotAsync.ts @@ -0,0 +1,8 @@ + +export default class FunctionNotAsync extends Error +{ + constructor(filename: string, functionName: string) + { + super(`Function '${functionName}' from file '${filename}' is not async`); + } +} diff --git a/packages/caching/src/segment/errors/InvalidFilename.ts b/packages/caching/src/segment/errors/InvalidFilename.ts new file mode 100644 index 00000000..42ea9194 --- /dev/null +++ b/packages/caching/src/segment/errors/InvalidFilename.ts @@ -0,0 +1,8 @@ + +export default class InvalidFilename extends Error +{ + constructor(filename: string) + { + super(`Segment filename '${filename}' is invalid`); + } +} diff --git a/packages/caching/src/segment/errors/MissingModuleExport.ts b/packages/caching/src/segment/errors/MissingModuleExport.ts new file mode 100644 index 00000000..b34353cf --- /dev/null +++ b/packages/caching/src/segment/errors/MissingModuleExport.ts @@ -0,0 +1,8 @@ + +export default class MissingModuleExport extends Error +{ + constructor(filename: string, key: string) + { + super(`Module '${filename}' does not export '${key}'`); + } +} diff --git a/packages/caching/src/segment/errors/ModuleNotFound.ts b/packages/caching/src/segment/errors/ModuleNotFound.ts new file mode 100644 index 00000000..e3125d9b --- /dev/null +++ b/packages/caching/src/segment/errors/ModuleNotFound.ts @@ -0,0 +1,8 @@ + +export default class ModuleNotLoaded extends Error +{ + constructor(filename: string) + { + super(`Segment module could not be loaded from '${filename}'`); + } +} diff --git a/packages/caching/src/segment/errors/NotAFunction.ts b/packages/caching/src/segment/errors/NotAFunction.ts new file mode 100644 index 00000000..d3a3b957 --- /dev/null +++ b/packages/caching/src/segment/errors/NotAFunction.ts @@ -0,0 +1,8 @@ + +export default class NotAFunction extends Error +{ + constructor(filename: string, exportKey: string) + { + super(`The export '${exportKey}' from file '${filename}' is not a function`); + } +} diff --git a/packages/caching/src/segment/index.ts b/packages/caching/src/segment/index.ts new file mode 100644 index 00000000..3aefb4d0 --- /dev/null +++ b/packages/caching/src/segment/index.ts @@ -0,0 +1,8 @@ + +export { default as Segment } from './models/Segment'; +export { default as SegmentModule } from './models/Module'; +export { default as SegmentProcedure } from './models/Procedure'; +export { default as SegmentImplementation } from './models/Implementation'; +export { default as Segmentation } from './models/Segmentation'; + +export { default as SegmentReader } from './Reader'; diff --git a/packages/caching/src/segment/models/Implementation.ts b/packages/caching/src/segment/models/Implementation.ts new file mode 100644 index 00000000..73885706 --- /dev/null +++ b/packages/caching/src/segment/models/Implementation.ts @@ -0,0 +1,38 @@ + +import type { ReflectionFunction } from '@jitar/reflection'; + +export default class Implementation +{ + #id: string; + #importKey: string; + #fqn: string; + #access: string; + #version: string; + #importDefault: boolean; + #executable: ReflectionFunction; + + constructor(id: string, importKey: string, fqn: string, access: string, version: string, importDefault: boolean, executable: ReflectionFunction) + { + this.#id = id; + this.#importKey = importKey; + this.#fqn = fqn; + this.#access = access; + this.#version = version; + this.#importDefault = importDefault; + this.#executable = executable; + } + + get id() { return this.#id; } + + get importKey() { return this.#importKey; } + + get fqn() { return this.#fqn; } + + get access() { return this.#access; } + + get version() { return this.#version; } + + get importDefault() { return this.#importDefault; } + + get executable() { return this.#executable; } +} diff --git a/packages/caching/src/segment/models/Import.ts b/packages/caching/src/segment/models/Import.ts new file mode 100644 index 00000000..76af24f8 --- /dev/null +++ b/packages/caching/src/segment/models/Import.ts @@ -0,0 +1,16 @@ + +export default class Import +{ + #members: string[]; + #from: string; + + constructor(members: string[], from: string) + { + this.#members = members; + this.#from = from; + } + + get members() { return this.#members; } + + get from() { return this.#from; } +} diff --git a/packages/caching/src/segment/models/Module.ts b/packages/caching/src/segment/models/Module.ts new file mode 100644 index 00000000..d2b82f05 --- /dev/null +++ b/packages/caching/src/segment/models/Module.ts @@ -0,0 +1,32 @@ + +import type Imports from '../types/Imports'; + +import type Implementation from './Implementation'; + +export default class Module +{ + #filename: string; + #location: string; + #imports: Imports; + #implementations: Implementation[] = []; + + constructor(filename: string, location: string, imports: Imports) + { + this.#filename = filename; + this.#location = location; + this.#imports = imports; + } + + get filename() { return this.#filename; } + + get location() { return this.#location; } + + get imports() { return this.#imports; } + + get implementations() { return this.#implementations; } + + addImplementation(implementation: Implementation): void + { + this.#implementations.push(implementation); + } +} diff --git a/packages/caching/src/segment/models/Procedure.ts b/packages/caching/src/segment/models/Procedure.ts new file mode 100644 index 00000000..0641bbf1 --- /dev/null +++ b/packages/caching/src/segment/models/Procedure.ts @@ -0,0 +1,23 @@ + +import type Implementation from './Implementation'; + +export default class Procedure +{ + #fqn: string; + #implementations: Implementation[] = []; + + constructor(fqn: string, implementations: Implementation[] = []) + { + this.#fqn = fqn; + this.#implementations = implementations; + } + + get fqn() { return this.#fqn; } + + get implementations() { return this.#implementations; } + + addImplementation(implementation: Implementation): void + { + this.#implementations.push(implementation); + } +} diff --git a/packages/caching/src/segment/models/Segment.ts b/packages/caching/src/segment/models/Segment.ts new file mode 100644 index 00000000..d85024e8 --- /dev/null +++ b/packages/caching/src/segment/models/Segment.ts @@ -0,0 +1,43 @@ + +import type Module from './Module'; +import type Procedure from './Procedure'; + +export default class Segment +{ + #name: string; + #modules: Module[]; + #procedures: Procedure[]; + + constructor(name: string, modules: Module[], procedures: Procedure[]) + { + this.#name = name; + this.#modules = modules; + this.#procedures = procedures; + } + + get name() { return this.#name; } + + get modules() { return this.#modules; } + + get procedures() { return this.#procedures; } + + hasModule(filename: string): boolean + { + return this.#modules.some(module => module.filename === filename); + } + + getModule(filename: string): Module | undefined + { + return this.#modules.find(module => module.filename === filename); + } + + hasProcedure(fqn: string): boolean + { + return this.#procedures.some(procedure => procedure.fqn === fqn); + } + + getProcedure(fqn: string): Procedure | undefined + { + return this.#procedures.find(procedure => procedure.fqn === fqn); + } +} diff --git a/packages/caching/src/segment/models/Segmentation.ts b/packages/caching/src/segment/models/Segmentation.ts new file mode 100644 index 00000000..e4698497 --- /dev/null +++ b/packages/caching/src/segment/models/Segmentation.ts @@ -0,0 +1,29 @@ + +import type Segment from './Segment'; + +export default class Segmentation +{ + #segments: Segment[]; + + constructor(segments: Segment[]) + { + this.#segments = segments; + } + + get segments() { return this.#segments; } + + getSegment(segmentName: string): Segment | undefined + { + return this.#segments.find(segment => segment.name === segmentName); + } + + isModuleSegmented(moduleFilename: string): boolean + { + return this.#segments.some(segment => segment.hasModule(moduleFilename)); + } + + getSegments(moduleFilename: string): Segment[] + { + return this.#segments.filter(segment => segment.hasModule(moduleFilename)); + } +} diff --git a/packages/caching/src/segment/types/File.ts b/packages/caching/src/segment/types/File.ts new file mode 100644 index 00000000..0d9ac5b4 --- /dev/null +++ b/packages/caching/src/segment/types/File.ts @@ -0,0 +1,6 @@ + +import SegmentImports from './Imports'; + +type File = Record; + +export default File; diff --git a/packages/caching/src/segment/types/ImportProperties.ts b/packages/caching/src/segment/types/ImportProperties.ts new file mode 100644 index 00000000..f9a6462c --- /dev/null +++ b/packages/caching/src/segment/types/ImportProperties.ts @@ -0,0 +1,9 @@ + +type ImportProperties = +{ + as?: string; + access?: string; + version?: string; +} + +export default ImportProperties; diff --git a/packages/caching/src/segment/types/Imports.ts b/packages/caching/src/segment/types/Imports.ts new file mode 100644 index 00000000..017b9478 --- /dev/null +++ b/packages/caching/src/segment/types/Imports.ts @@ -0,0 +1,6 @@ + +import ImportProperties from './ImportProperties'; + +type Imports = Record; + +export default Imports; diff --git a/packages/caching/src/utils/FileHelper.ts b/packages/caching/src/utils/FileHelper.ts new file mode 100644 index 00000000..7b0525a0 --- /dev/null +++ b/packages/caching/src/utils/FileHelper.ts @@ -0,0 +1,76 @@ + +const DEFAULT_EXTENSION = 'js'; +const EXTENSION_PATTERN = /\.js$/; + +export default class FileHelper +{ + static translatePath(filename: string) + { + const parts = filename.split('/'); + const translated = []; + + translated.push(parts[0]); + + for (let index = 1; index < parts.length; index++) + { + const part = parts[index].trim(); + + switch (part) + { + case '': continue; + case '.': continue; + case '..': translated.pop(); continue; + } + + translated.push(part); + } + + return translated.join('/'); + } + + static makePathRelative(absoluteFilename: string, relativeToPath: string): string + { + const absoluteFilenameParts = absoluteFilename.split('/'); + const relativeToParts = relativeToPath.split('/'); + + while (absoluteFilenameParts[0] === relativeToParts[0]) + { + absoluteFilenameParts.shift(); + relativeToParts.shift(); + } + + const relativePath = relativeToParts.map(() => '..').join('/'); + + const prefix = relativeToParts.length > 0 ? relativePath : '.'; + const suffix = absoluteFilenameParts.join('/'); + + return `${prefix}/${suffix}`; + } + + static makePathAbsolute(relativeFilename: string, relativeToPath: string): string + { + const concatenated = `${relativeToPath}/${relativeFilename}`; + + return this.translatePath(concatenated); + } + + static extractPath(filename: string) + { + return filename.split('/').slice(0, -1).join('/'); + } + + static extractFilename(filename: string) + { + return filename.split('/').pop(); + } + + static assureExtension(filename: string): string + { + return filename.endsWith(`.${DEFAULT_EXTENSION}`) ? filename : `${filename}.${DEFAULT_EXTENSION}`; + } + + static addSubExtension(filename: string, subExtension: string): string + { + return filename.replace(EXTENSION_PATTERN, `.${subExtension}.${DEFAULT_EXTENSION}`); + } +} diff --git a/packages/caching/src/utils/IdGenerator.ts b/packages/caching/src/utils/IdGenerator.ts new file mode 100644 index 00000000..d3458e44 --- /dev/null +++ b/packages/caching/src/utils/IdGenerator.ts @@ -0,0 +1,10 @@ + +export default class IdGenerator +{ + #id = 0; + + next(): string + { + return `$${++this.#id}`; + } +} diff --git a/packages/caching/src/utils/index.ts b/packages/caching/src/utils/index.ts new file mode 100644 index 00000000..b80860cb --- /dev/null +++ b/packages/caching/src/utils/index.ts @@ -0,0 +1,3 @@ + +export { default as IdGenerator } from './IdGenerator'; +export { default as FileHelper } from './FileHelper';