1

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!
1
  • Since apply is a function's/method's trap it is of no use for an object like the TelegramClient instance. Thus either the instance's sendMessage has to be proxified or one is in need of another approach. Commented Aug 21, 2023 at 11:16

3 Answers 3

0

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>

0

The main problem here is that apply is not async, marking async won't change that.

Rather than trying to intercept the sendMessage method, another option is to auto-wrap the function inside the get.

Below is an example, I've also use a Set to make sure during get the method does not get wrapped again. Also added a connected property, as sending another message shouldn't require another connect.

Update:

Keeping track of instances and proxy private data, is a little bit more involved than you might think. The context of this with a proxy will change to the proxy, (makes sense), so below I've updated to hopefully account for multiple instances, and keeping data private no matter if your going via the proxy or accessing the target directly. The trick here is to use a Symbol to store the proxies original target, you can then use a weakMap to keep your private instance data there.

const PROXY_TARGET = Symbol('PROXY_TARGET');

const _privData = new WeakMap();
const privData = t => {
  let ret = _privData.get(t[PROXY_TARGET] ?? t);
  if (!ret) {
    ret = {
      connected: false,
      wrapped: new Set()
    }
    _privData.set(t, ret);
  }
  return ret;
}


class TelegramClient {
  async connect() {
    const priv = privData(this);
    if (priv.connected) return;
    priv.connected = true;
    console.log("Connecting to Telegram...");
    // Simulate some asynchronous work
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log("Connected!");
  }
  async sendMessage(message) {    
    console.log(`Sending message: ${message}`);
  }
}

const handler = {
  get(target, prop, receiver) {
    const priv = privData(target);
    if (prop === PROXY_TARGET) return target;
    if (['connect'].includes(prop) || priv.wrapped.has(prop) ) {
      return Reflect.get(...arguments);
    }
    const fn = target[prop];
    function wrap() {
      return target.connect().then(() => {
        return fn(...arguments);
      });
    }
    priv.wrapped.add(prop);
    target[prop] = wrap;
    return target[prop];
  }
}

async function test(autoConnect) {
  console.log(autoConnect ? 'Auto Connect' : 'Manual Connect');
  const telegramClient = new TelegramClient();
  const proxiedTelegramClient = new Proxy(telegramClient, handler);
  if (!autoConnect) await proxiedTelegramClient.connect();
  await proxiedTelegramClient.sendMessage("Hello, Telegram!");
  await proxiedTelegramClient.sendMessage("Another message.");
}

//let try auto and manual connect.
test(true).then(() => test(false));
.as-console-wrapper { min-height: 100%!important; top: 0; }

0
-1

this can be resolved using a "get" instead of "apply" handler

//... previous code before the proxiedTelegramClient
// Create a Proxy for the Telegram client
const proxiedTelegramClient = new Proxy(telegramClient, {
  get(target, property) {
    if (property === 'sendMessage') {
      return async function (...args) {
        await target.connect(); // Connect to Telegram before sending the message
        return Reflect.apply(target[property], target, args);
      };
    }
    return target[property];
  },
});

but if you want to keep code as it is, you can mak use of

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

  async sendMessage(message: string) {
    await this.connect(); // Connect to Telegram before sending the message
    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) {
    return Reflect.apply(target, thisArg, argumentsList);
  },
});

(async () => {
  await proxiedTelegramClient.sendMessage("Hello, Telegram!");
})();

hope it helps

1
  • The get trap will be executed already for just accessing a property. The OP might not want to establish a connection by just accessing the instance's sendMessage property. The OP's goal seems to be establishing a connection only in case the sendMessage method is being invoked. Commented Aug 21, 2023 at 11:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.