What is the process for dynamically loading a concrete implementation and customizing object creation through parameterization?

Concern

I am currently developing a Chrome Extension that is capable of receiving commands and is built using TypeScript. My objective is to separate out concrete implementations of a class into individual files that can be loaded dynamically.

https://i.sstatic.net/9h99g.jpg

Illustration

Within my Chrome extension, I receive JSON data in the following format.

{
    "action": "get_tasking",
    "tasks": [
        {
            "command": "ConcreteContentCommand",
            "parameters": "{'param1': 'bla', 'param2': 'bla2'}",
            "timestamp": 1578706611.324671, //to aid in ordering
            "id": "task uuid",
        }
    ]
}

The initial step would involve loading the appropriate file from IndexedDB, which could potentially be named ConcreteContentCommand.js, housing the implementation for ConcreteContentCommand. Following this, I aim to instantiate ConcreteContentCommand with the provided parameters param1, param2, subsequently utilizing it within my invoker.

class ContentReceiver {
  targetURL: string;
  payload: string;

  constructor(targetURL: string, payload: string) {
    this.targetURL = targetURL;
    this.payload = payload;
  }

  InjectRunnable() {
    // insert payload into tab
    console.log(`Do something with ${this.payload}`);
  }
}

interface Command {
  execute(): void;
}

abstract class ContentCommand implements Command {
  receiver: ContentReceiver;

  protected constructor(url: string) {
    this.receiver = new ContentReceiver(url, this.runnable.toString());
  }

  abstract runnable(): void;

  execute() {
    this.receiver.InjectRunnable();
  }
}

abstract class BackgroundCommand implements Command {
  abstract runnable(): void;

  execute() {
    this.runnable();
  }
}

// Extraction of this class needed as extension remains unaware of its existence
class ConcreteContentCommand extends ContentCommand {
  param1: string;
  param2: string;

  constructor(param1: string, param2: string) {
    super("https://staticurl");
    this.param1 = param1;
    this.param2 = param2;
  }

  runnable() {
    // Implement specific functionality here
  }
}

What are the various approaches available to accomplish this task?

Answer №1

Great job kicking things off with a UML diagram!

In an ideal scenario using the command pattern, you should be able to execute any command without prior knowledge of the specific commands or their availability. This is because each command acts as a self-contained object. However, if you are creating a command instance based on stringified data, there comes a point where you need to map a string name to its corresponding class implementation.

Your UML includes a reference to a CommandListener. Although I'm not entirely sure about the server-side execution details, it might be beneficial for each ConcreteCommand to attach its own listener. This listener could then listen to all commands and handle those that match its specified name.

Another approach could involve having a centralized TaskParser, responsible for converting all JSON actions into Command objects and assigning them to the invoker (referred to as

CommandRunner</code in this case). Each <code>ConcreteCommand
would need to register itself with the TaskParser.

When considering what a Command requires to be created from a JSON string, a generic StringableCommand dependent on parameters Args was initially conceptualized. It features a name attribute for identification purposes, can be constructed based on Args, and includes a method called validateArgs, which acts as a type guard. If validateArgs returns true, the args array is confirmed to belong to type

Args</code, thereby making it safe to invoke <code>new
.

interface StringableCommand<Args extends any[]> extends Command {
    new(...args: Args): Command;
    name: string;
    validateArgs( args: any[] ): args is Args;
}

It's important to note that this interface applies to the class prototype rather than the class instance, which can lead to some confusion. Therefore, we refrain from specifying that our class extends StringableCommand. To make validateArgs accessible via the prototype, it must be implemented as a static method. As per JavaScript principles, we do not need to explicitly implement name since it already exists.

However, employing generics within StringableCommand may complicate matters for our TaskParser, which deals with multiple command types. Thus, defining a constructible command as one featuring a fromArgs method that accepts an array of unknown arguments and either returns a Command or raises an Error could potentially simplify the process. The class could handle validation internally, or rely on the constructor to throw errors (which TypeScript would need to ignore).

interface StringableCommand {
    name: string;
    fromArgs( ...args: any[] ): Command; // or throw an error
}

The JSON string being parsed exhibits the following structure:

interface ActionPayload {
    action: string;
    tasks: TaskPayload[];
}

interface TaskPayload {
    command: string;
    parameters: any[];
    timestamp: number;
    id: string;
}

For ease of passing parameters to the command constructor, I've altered your parameters structure to be an array instead of a keyed object.

A simplified version of our TaskParser intended to resolve these commands resembles the snippet provided below:

class TaskParser {

    private readonly runner: CommandRunner;
    private commandConstructors: Record<string, StringableCommand> = {}
    private errors: Error[] = [];

    constructor(runner: CommandRunner) {
        this.runner = runner;
    }

    public registerCommand( command: StringableCommand ): void {
        this.commandConstructors[command.name] = command;
    }

    public parseJSON( json: string ): void {
        const data = JSON.parse(json) as ActionPayload;
        data.tasks.forEach(({command, parameters, id, timestamp}) => {
            const commandObj = this.findCommand(command);
            if ( ! commandObj ) {
                this.storeError( new Error(`invalid command name ${command}`) );
                return;
            }
            try {
                const c = commandObj.fromArgs(parameters);
                this.runner.addCommand(c, timestamp, id);
            } catch (e) { // catch errors thrown by `fromArgs`
                this.storeError(e);
            }
        });
    }

    private findCommand( name: string ): StringableCommand | undefined {
        return this.commandConstructors[name];
    }

    private storeError( error: Error ): void {
        this.errors.push(error);
    }

}

This class functions alongside a placeholder CommandRunner which currently serves as a stub due to insufficient insight into the expected system behavior. In practice, this would be the component responsible for invoking/executing the commands.

class CommandRunner {

    addCommand( command: Command, timestamp: number, id: string ): void { // whatever args you need
        // perform necessary operations
    }
}

For further exploration and experimentation, feel free to check out the Typescript Playground Link.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Develop a custom data structure by combining different key elements with diverse property values

Within my coding dilemma lies a Union of strings: type Keys = 'a' | 'b' | 'c' My desire is to craft an object type using these union keys, with the flexibility for assigned values in that type. The typical approach involves ...

Accessing Properties or Methods in Angular2 Components

My application consists of 3 main components: PageMenu, LoginSession, and LoginForm. The purpose is to establish a connection between the variables in LoginSession and PageMenu, allowing for the proper functionality of the LoginForm component. PageMenu: ...

Using TypeScript to Add Items to a Sorted Set in Redis

When attempting to insert a value into a sorted set in Redis using TypeScript with code like client.ZADD('test', 10, 'test'), an error is thrown Error: Argument of type '["test", 10, "test"]' is not assigna ...

Retrieving a pair of data points from a Vue <select> event when it changes

I am facing an issue with a dropdown menu that is using an array with key:value pairs. I want the dropdown to only show values, but when a selection is made, I need to pass both the value and the key using the @change event. <select @change=" ...

Unsure about Typescript object structures {} and []?

I am new to using lists and object lists in Typescript and I'm unsure of how they function. In the code snippet below, a few objects are created and some temporary values are assigned to them through a loop. However, my goal is to have the console log ...

Typescript's forEach method allows for iterating through each element in

I am currently handling graphql data that is structured like this: "userRelations": [ { "relatedUser": { "id": 4, "firstName": "Jack", "lastName": "Miller" }, "type": "FRIEND" }, { "relatedUser": ...

The onClick function for a button is not functioning properly when using the useToggle hook

When the button is clicked, it toggles a value. Depending on this value, the button will display one icon or another. Here is the code snippet: export const useToggle = (initialState = false) => { const [state, setState] = useState(initialState); c ...

Ways to validate email input with pattern in Angular 2

I need help figuring out how to use the email pattern error for validation using the hasError function in Angular 2. My goal is to apply the invalid class to my input field. Below, you can see the code from registration.component.html: <div class="inpu ...

The response code in the API remains 200 despite setting the status code to 204 in NestJS

I have developed an API that needs to return a 204 - No Content Response import { Controller, Get, Header, HttpStatus, Req, Res } from '@nestjs/common'; import { Response } from 'express'; @Get("mediation-get-api") @Head ...

Step-by-step guide on how to index timestamp type using Knex.js

I'm in the process of indexing the created_at and updated_at columns using knex js. However, when I try to use the index() function, I encounter the following error: Property 'index' does not exist on type 'void' await knex.sche ...

Tips on transitioning a Node.js application from JavaScript to TypeScript incrementally

I have a JavaScript node application that has grown quite large and I am considering migrating to TypeScript for faster development and easier code maintenance. I have installed TypeScript along with the node and mocha types using the following commands: ...

Tips for preventing repetition in http subscribe blocks across various components

Imagine a scenario where there is a service used for making HTTP request calls. There are two different components (which could be more than two) that need to send the same request using the same observables via this service. After receiving the result, it ...

Different ways to determine if a given string exists within an Object

I have an object called menu which is of the type IMenu. let menu: IMenu[] = [ {restaurant : "KFC", dish:[{name: "burger", price: "1$"}, {name: "french fries", price: "2$"}, {name: "hot dog", d ...

Extend the row of the table according to the drop-down menu choice

I am working on a feature where a dropdown menu controls the expansion of rows in a table. Depending on the option selected from the dropdown, different levels of items need to be displayed in the table. For example, selecting level 1 will expand the first ...

Unlock hidden Google secrets within your Angular application using Google Secret Manager

Can the Google Secret Manager API be accessed with a simple API call using an API key? https://secretmanager.googleapis.com/v1/projects/*/secrets/*?key=mykey returns a 401 unauthenticated error. While the Node.js server powering the Angular app uses the c ...

Transferring client-side data through server functions in Next.js version 14

I am working on a sample Next.js application that includes a form for submitting usernames to the server using server actions. In addition to the username, I also need to send the timestamp of the form submission. To achieve this, I set up a hidden input f ...

Arrangement of code: Utilizing a Node server and React project with a common set of

Query I am managing: a simple react client, and a node server that functions as both the client pages provider and an API for the client. These projects are tightly integrated, separate TypeScript ventures encompassed by a unified git repository. The se ...

Troubleshooting problem with Angular Click Outside Directive and unexpected extra click event issue

The challenge I'm facing involves implementing a custom Click Outside Directive for closing modal dialogs, notifications, popovers, and other 'popups' triggered by various actions. One specific issue is that when using the directive with pop ...

Is the validity of the expression !args.value || args.value.length true?

After analyzing this segment of code, I noticed an interesting expression: !args.value || args.value.length For instance, consider the following scenario: let v = {}; console.log(!v.value); //outputs true console.log(v.value); //outputs undefined con ...

Ensure that you call setState prior to invoking any other functions

Is there a way to ensure that the setSearchedMovie function completes before executing the fetchSearchedMovie(searchedMovie) function? const { updateMovies, searchedMovie, setSearchedMovie } = useContext(MoviesContext); const fetchMoviesList = (ev ...