Search code examples
javascriptnode.jstypescriptts-node

How to manage async singleton data which is required before the application can function?


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.


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
        }
    }