Skip to content

Commit

Permalink
#296: first draft of the new cache model
Browse files Browse the repository at this point in the history
  • Loading branch information
petermasking committed Aug 15, 2024
1 parent 6a2730c commit de66ec4
Show file tree
Hide file tree
Showing 38 changed files with 1,360 additions and 1 deletion.
32 changes: 32 additions & 0 deletions packages/caching/src/Builder.ts
Original file line number Diff line number Diff line change
@@ -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<void>
{
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);
}
}
27 changes: 27 additions & 0 deletions packages/caching/src/application/Builder.ts
Original file line number Diff line number Diff line change
@@ -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<void>
{
await Promise.all([
this.#moduleBuilder.build(application),
this.#segmentBuilder.build(application)
]);
}
}
22 changes: 22 additions & 0 deletions packages/caching/src/application/ClassSourceBuilder.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
171 changes: 171 additions & 0 deletions packages/caching/src/application/ImportRewriter.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions packages/caching/src/application/LocalModuleBuilder.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
}
88 changes: 88 additions & 0 deletions packages/caching/src/application/ModuleBuilder.ts
Original file line number Diff line number Diff line change
@@ -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<void>
{
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<void>
{
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<void>
{
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<void>
{
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<void>
{
// 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);
}
}
28 changes: 28 additions & 0 deletions packages/caching/src/application/Reader.ts
Original file line number Diff line number Diff line change
@@ -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<Application>
{
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);
}
}
Loading

0 comments on commit de66ec4

Please sign in to comment.