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.

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

After extraction from local storage, the type assertion is failing to work properly

I have a unique situation in my Angular project where I have stored an array of data points in the local storage. To handle this data, I have created a custom class as follows: export class Datapoint { id: number; name: string; // ... additional pr ...

Following the build in Angular, it only displays the index.html file and a blank screen

According to the information on Angular's official website, running the "ng build" command should generate files in the dist folder ready for hosting. However, after running this command, the index.html file is empty except for the page title. When yo ...

Trustpilot: The function window.Trustpilot.loadFromElement does not exist in Safari

I am looking to integrate 3 TrustPilots into my Angular application. import { Component, Input, OnInit } from '@angular/core'; declare global { interface Window { Trustpilot: any; } } window.Trustpilot = window.Trustpilot || {}; @Component ...

Output Scalable Vector Graphics (SVG) content on a webpage

I need to include an SVG element in my Angular 2+ code. My goal is to provide users with the option to print the SVG element as it appears on the screen. <div class="floor-plan" id="printSectionId2" (drop)="onDrop($event)" (dragover)="onDragOver ...

Challenges encountered when using promises for handling mysql query results

I've been working on creating a function that will return the value of a mysql query using promises. Here's what I have so far: query(query: string): string { var response = "No response..."; var sendRequest = (query:string): Prom ...

learning how to transfer a value between two different components in React

I have 2 components. First: component.ts @Component({ selector: "ns-app", templateUrl: "app.component.html", }) export class AppComponent implements OnInit { myid: any; myappurl: any; constructor(private router: Router, private auth: ...

arrange elements by their relationship with parents and children using typescript and angular

Here is a list that needs to be sorted by parent and child relationships: 0: {id: 7, name: "333", code: "333", type: 3, hasParent: true, parentId: 4} 1: {id: 6, name: "dfgdfg", code: "dfgdfg", type: 3, hasParent: false, parentId: null} 2: {id: 5, name: ...

Unable to successfully remove item using Asyncstorage

const deleteProduct = prod => { Alert.alert( 'Delete Product', `Are you sure you want to remove ${prod.id}?`, [ { text: 'Cancel', style: 'cancel', }, { ...

Retrieving the selected date from mat-datepicker into a FormControl

When creating a POST request to an API, I encountered an issue with the mat-datepicker field as it throws an error when inside the ngOnInit() call (since nothing is selected yet). Other fields like name, email, etc. work fine, but extracting a value from t ...

Obtain JSON information in a structured model layout using Angular 4

I have different categories in the backend and I would like to retrieve them in a model format. Here is how my model is structured: export class Category { name: string; id : string; } And this is how the data appears in the backend: { "name": "cars", ...

Guide to transforming an embed/nested FormGroup into FormData

Presenting my Form Group: this.storeGroup = this.fb.group({ _user: [''], name: ['', Validators.compose([Validators.required, Validators.maxLength(60)])], url_name: [''], desc: ['', Validators.compose([Valida ...

Using TypeScript to define task invocation parameters with AWS CDK's CfnMaintenanceWindowTask

Currently, I am utilizing AWS CDK along with the library @aws-cdk/aws-ssm and TypeScript to construct CfnMaintenanceWindowTask. The code example I am working on is derived from AWS CloudFormation documentation, specifically for "Create a Run Command t ...

Transferring information to a deep-level interface

I am currently working on creating an object that aligns with my interface structure. Success Story export interface ServiceDataToDialog { id: number, service: string, } constructor(private _dialogRef: MatDialogRef<DialogServiceTabletAddRowComp ...

Is it possible to incorporate regular React JSX with Material UI, or is it necessary to utilize TypeScript in this scenario?

I'm curious, does Material UI specifically require TypeScript or can we use React JSX code instead? I've been searching for an answer to this question without any luck, so I figured I'd ask here. ...

The useEffect hook is failing to resolve a promise

I have received a response from an API that I need to display. Here is a snippet of the sample response (relevant fields only): [ { ...other fields, "latitude": "33.5682166", "longitude": "73 ...

Encountering issues with utilizing global variables in Ionic 3

Using Ionic 3, I created a class for global variables but encountered an error Uncaught (in promise): Error: No provider for Globals! Error: No provider for Globals! at injectionError (http://localhost:8100/build/vendor.js:1590:86) at noProviderError Th ...

Is there a way to determine if an npm package is compatible with a specific version of Angular

As I work on my project, I realize that I have many dependencies on libraries that support Angular2 but not Angular6. It can be challenging to determine if a library supports Angular2 from just reading their GitHub pages. One idea is to check the package ...

When attempting to utilize the dispatch function in a class-based component, an invalid hook call error may

I am a beginner with react-redux. I currently have this code that uses react, redux, and TypeScript. The code utilizes a class-based component and I am attempting to use dispatch to trigger an action to increment the value of counter. However, I encountere ...

Error: The JSON file cannot be located by the @rollup/plugin-typescript plugin

I have recently set up a Svelte project and decided to leverage JSON files for the Svelte i18n package. However, I am facing challenges when trying to import a JSON file. Although the necessary package is installed, I can't figure out why the Typescri ...

Step-by-step guide on deploying Angular Universal

Exploring Angular universal and working on understanding deployment strategies. Check out the Github repository at https://github.com/angular/universal-starter This project includes Angular 2 Universal, TypeScript 2, and Webpack 2. After running the comm ...