I've been struggling to come up with this system architecture for a while now and got to a point where I thought it was perfect. The whole specification is fairly straightforward. We have something called an Extensions
, holding core functionalities to interact with an API:
export abstract class Extension<ConfigType extends object, DataType extends object> {
constructor(public definition: ExtensionDefinition<ConfigType, DataType>) {
this.definition = definition;
}
abstract initilize(): this;
abstract APIFetchSessionByID(sessionId: string) : Promise<ExtensionSession<ConfigType, DataType>>;
abstract APIFetchSessionByToken(token: string) : Promise<ExtensionSession<ConfigType, DataType>>;
abstract doSomethingAmazing() : any;
}
We then come into the main problem known as the ExtensionDefinition
, holding the raw JSON data about the the extension we are creating: configuration
, data
, name
, description
....
export class ExtensionDefinition<ConfigType extends object, DataType extends object> implements IExtensionProperties<ConfigType, DataType> {
constructor(data: IExtensionProperties<ConfigType, DataType>) {
Object.assign(this, data);
}
}
The problem occurs when we realise that the data passed in the ExtensionDefinition
is asynchronous and stored on a third-party API and only needs to be obtained once or until the definition is changed. My current solution stores this data in a .JSON and I simply pass the data from the JSON file directly into the ExtensionDefinition
constructor where I plan on updating the third-party API on start-up. This works but causes lots more errors than it solves. For example, the ExtensionDefinition
data retrieved from the API is configurable via the API so if anything new is introduce, a fix needs to be implemented.
For further context, I have a service that manages and maintains all these extensions:
APIApplication.initilize({
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
devToken: process.env.DEV_TOKEN!,
tokenURL: "...URL",
authorizationURL: "...URL",
extensions: [
new ExampleExtension(),
new ExampleExtension(),
new ExampleExtension(),
...MORE
]
});
A lot of other workarounds ive thought of have worked better but get slowed down by JavaScript inability to await asynchronous data in a class constructor. Some include passing the ExtensionDefinition
as a promise and awaiting the property when needed which works but isnt ideal as I would need to obtain each definition individually from the API, resulting in a lot of overhead.
The entire application depends on these extensions so without them it wouldnt function. I can imagine this is a common problem that many developers have encountered and may have already tackled this problem and come up with a better solution.
After a few iterations I came to the conclusion that rather than storing the definitions in memory on first start up I could essentially just remove the problem. Now rather than passing an ExtensionDefinition
into an Extension
I instead inject the application into the Extension
like so:
export abstract class Extension<TConfig extends object, TData extends object> implements IExtension<TConfig, TData> {
constructor(public application: IApplication){
this.application = application;
}
abstract initilize() : Promise<this> | this;
abstract APIFetchSessionByID(sessionId: string): Promise<ISession<TConfig, TData>> | ISession<TConfig, TData>;
abstract APIFetchSessionByToken(token: string): Promise<ISession<TConfig, TData>> | ISession<TConfig, TData>;
}
Now an application can do whatever it wishes to retrieve a definition, chaching the definition in memory if possible on first retrival. Something like this works well:
export class APIApplication implements IApplication{
private _definitions = new Map();
constructor(data: IApplicationProperties){
this.clientId = data.clientId
this.clientSecret = data.clientSecret;
this.devToken = data.devToken;
this.tokenURL = data.tokenURL;
this.authorizationURL = data.authorizationURL;
}
async GetDefinition<TConfig extends object, TData extends object>(extensionId: string){
...DO API THINGS
}
async CachedGetDefinition<TConfig extends object, TData extends object>(extensionId: string){
...DO API THINGS
}
}