0

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.

user13020816
  • 43
  • 2
  • 5
  • "*get slowed down by JavaScript inability to await asynchronous data in a class constructor.*" - then [don't use the constructor for that](https://stackoverflow.com/a/24686979/1048572). Replace `new ExampleExtension()` by `await ExampleExtension.loadDefinitionAndCreateExtension()`. – Bergi Aug 12 '23 at 00:44
  • "*the `ExtensionDefinition` data retrieved from the API is configurable via the API so if anything new is introduce, a fix needs to be implemented.*" - I don't see what this has to do with the architecture of your application and its extensions. This is *always* a problem when working with third-party APIs. – Bergi Aug 12 '23 at 00:46
  • I believe I did toy with using an `init()` function but didnt like how the application would need to be wrapped in an anonymous async function in one way or the other. With regards to to how the API works, in some cases the API's may _never_ change but this starts to get off topic. I eventually came to a solution that removes the need for the definitions on start-up with further improvements as a by-product. Thank you for the advice though. – user13020816 Aug 12 '23 at 01:11
  • You don't need a wrapper async function if you use top-level await in ES modules. But either way, it sounds like you *want* to wait for the extension definitions to load at startup, so I don't see anything wrong with that. – Bergi Aug 12 '23 at 14:21

1 Answers1

0

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
    }
}
user13020816
  • 43
  • 2
  • 5