javascriptfunctioninterceptorcontrol-flowes6-proxy

How to always apply a method before executing another method via a proxy?


I have the following code where I want to invoke the instance method connect before proceeding with the invocation of every other instance method of the TelegramClient class. How can this be achieved by making use of Proxy. As of now the connect method does not get invoked.

class TelegramClient {
  async connect() {
    console.log("Connecting to Telegram...");
    // Simulate some asynchronous work
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log("Connected!");
  }

  sendMessage(message: string) {
    console.log(`Sending message: ${message}`);
  }
}

const telegramClient = new TelegramClient();

// Create a Proxy for the Telegram client
const proxiedTelegramClient = new Proxy(telegramClient, {
  async apply(target, thisArg, argumentsList) {
    await target.connect(); // Connect to Telegram before invoking the method
    const result = Reflect.apply(target, thisArg, argumentsList);
    return result;
  },
});

proxiedTelegramClient.sendMessage("Hello, Telegram!");

The expected output was ...

Connecting to Telegram...
Connected!
Sending message: Hello, Telegram!

Solution

  • A viable approach which does not utilize a proxy was to use a generic implementation of an async around method-modifier. The latter can be seen as a specialized case of function-wrapping.

    Such a modifier accepts two functions, proceed and handler as well as a target-object as its 3 parameters. It does return an async function which again is going to return the awaited result of the (assumed async) handler (callback) function. The latter does get invoked within the context of the (optionally) provided target while also getting passed the proceed-function, its own handler-reference and the modified function's arguments-array.

    Thus, based on such a modifier, the OP could achieve the expected behavior by modifying e.g. a client instance's sendMessage-method like this ...

    // client instantiation.
    const telegramClient = new TelegramClient;
    
    // client's handler modification.
    telegramClient.sendMessage = asyncAround(
      telegramClient.sendMessage,
      ensureConnectedClient,
      telegramClient,
    );
    

    ... where ensureConnectedClient is the handler function which implements exactly what the OP is looking for ...

    "... [ensure a] connect[ed client] before proceeding with the invocation of every other instance method of the TelegramClient class"

    ... Example code ...

    // implementation of the client specific async `around`-handler.
    async function ensureConnectedClient(proceed, handler, args) {
      const client = this;
    
      // see:
      // - [https://gram.js.org/beta/classes/TelegramClient.html#connected]
      // - [https://gram.js.org/beta/classes/TelegramClient.html#disconnected]
    
      // ... always ensure a connected client ...
    
      if (client.disconnected()) {
        console.log('+++ client needs to be reconnected +++');
    
        await client.connect();
      } else {
        console.log('+++ client is still connected +++');
      }
    
      // ... before proceeding with any other method.
    
      return (
        await proceed.apply(client, args)
      );
    }
    
    // client instantiation.
    const telegramClient = new TelegramClient;
    
    // client's method-modification.
    telegramClient.sendMessage = asyncAround(
      telegramClient.sendMessage,
      ensureConnectedClient,
      telegramClient,
    );
    
    // client's modified handler invocation.
    telegramClient.sendMessage("Hello, Telegram!");
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    
    // client implementation.
    class TelegramClient {
      async connect() {
        console.log("Connecting to Telegram...");
    
        // simulate some asynchronous work.
        await new Promise(resolve => setTimeout(resolve, 1500));
    
        console.log("Connected!");
      }
      disconnected() {
        // simulate a connect-state method via random boolean.
        return Boolean(Math.floor(Math.random() + 0.5));
      }
      sendMessage(message) {
        console.log(`Sending message: ${ message }`);
      }
    }
    
    // implementation of an async `around` modifier.
    function asyncAround(proceed, handler, target) {
      return async function (...args) {
    
        return (
          await handler.call(target ?? null, proceed, handler, args)
        );
      };
    }
    
    </script>