Communication via JSON-RPC

I tried to follow the tutorial here on communication via JSON-RPC and write my own example but I’m having an issue. Communication from backend to frontend works but not the other way around.

This is how I did it:

// greeter-protocol.ts
export interface GreeterClient {
  onGreet(greetings: string): void;
}
export const GreeterServer = Symbol("GreeterServer");
export interface GreeterServer extends JsonRpcServer<GreeterClient> {
  greet(greetings: string): void;
  getGreeterName(): Promise<string>;
}

I haven then the backend implementation like so:

// greeter-backend-module.ts
export default new ContainerModule(bind => {
  bind(GreeterServer)
    .to(GreeterServerNode)
    .inSingletonScope();
  bind(BackendApplicationContribution)
    .to(GreeterBackendContribution)
    .inSingletonScope();
  bind(ConnectionHandler)
    .toDynamicValue(
      ctx =>
        new JsonRpcConnectionHandler<GreeterClient>(
          "/services/greeter",
          client => {
            const greeterServer = ctx.container.get<GreeterServer>(
              GreeterServer
            );
            greeterServer.setClient(client);
            return greeterServer;
          }
        )
    )
    .inSingletonScope();
});

@injectable()
class GreeterServerNode implements GreeterServer {
  client: GreeterClient | undefined;

  async getGreeterName(): Promise<string> {
    return "GreeterServerNode";
  }

  greet(greetings: string): void {
    this.client?.onGreet(greetings);
  }

  dispose(): void {}

  setClient(client: GreeterClient | undefined): void {
    this.client = client;
  }
}

@injectable()
class GreeterBackendContribution implements BackendApplicationContribution {
  @inject(GreeterServer) greeterServer: GreeterServer;

  initialize() {
    setInterval(() => {
      this.greeterServer.greet("Hello from backend module");
    }, 1000);
  }
}

and then the frontend module:

// greeter-frontend-module.ts
export default new ContainerModule((bind, unbind, isBound, rebind) => {
  bind(GreeterServer).toDynamicValue(ctx => {
    const greeterClient: GreeterClient = {
      onGreet: (greetings: string) => console.log("received greetings: " + greetings)
    };
    const connection = ctx.container.get(WebSocketConnectionProvider);
    return connection.createProxy<GreeterServer>(
      "/services/greeter",
      greeterClient
    );
  });
  bind(FrontendApplicationContribution).to(GreeterFrontendContribution);
});

@injectable()
export class GreeterFrontendContribution
  implements FrontendApplicationContribution {
  @inject(GreeterServer) greeterServer: GreeterServer;

  initialize() {
    setInterval(
      () =>
        this.greeterServer
          .getGreeterName()
          .then(result => console.log("GreeterServer.name=" + result))
          .catch(error => console.log("Failed to get greeter name", error)),
      2000
    );
  }
}

So the GreeterClient in the frontend module receives the greetings from the backend module sent at 1s internal, but when the frontend module calls this.greeterServer.getGreeterName() I get an error in the proxy:

root INFO Failed to get greeter name Error: Request 'getGreeterName' failed
    at Proxy.<anonymous> (http://localhost:3000/bundle.js:57352:33)
    at http://localhost:3000/29.bundle.js:56:14

Any idea what I’m doing wrong here?

[original thread by Hanksha]

[Alex Tugarev]

Hi @hanksha! The config looks good so far, but you’re trying to use the JSON-RPC connection before the frontend application has finished its initialization. Try to use onStart(app) instead, also inject FrontendApplicationStateService, and initialize on ready: stateService.onStateChanged(state => {if (state === ‘ready’) { this.myInitialize(); }});

[Hanksha]

Hi @alex-tugarev , that did not fix it, I debugged it a bit and the complete error is:

Error: Unhandled method getGreeterName
    at handleResponse (http://localhost:3000/bundle.js:130990:36)
    at processMessageQueue (http://localhost:3000/bundle.js:130814:9)
    at http://localhost:3000/bundle.js:130797:7
    at run (http://localhost:3000/bundle.js:127476:13)
    at runIfPresent (http://localhost:3000/bundle.js:127505:21)
    at onGlobalMessage (http://localhost:3000/bundle.js:127545:17)

[Hanksha]

It seems there is no request handlers registered for that method name

[Hanksha]

[Hanksha]

[Hanksha]

Found a fix

[Hanksha]

if I change the GreeterServerNode.getGreeterName to this:

getGreeterName = () => {
    console.log("GreeterServerNode.getGreeterName called");
    return Promise.resolve("GreeterServerNode");
  }

[Hanksha]

it works

[Hanksha]

if I just define it as a function of the class it doesn’t work

[Hanksha]

because here

[Hanksha]

// proxy-factory.js
listen(connection: MessageConnection): void {
        if (this.target) {
            for (const prop in this.target) {
                if (typeof this.target[prop] === 'function') {
                    connection.onRequest(prop, (...args) => this.onRequest(prop, ...args));
                    connection.onNotification(prop, (...args) => this.onNotification(prop, ...args));
                }
            }
        }
        connection.onDispose(() => this.waitForConnection());
        connection.listen();
        this.connectionPromiseResolve(connection);
    }

[Hanksha]

the line for (const prop in this.target) won’t include getGreeterName method

[Hanksha]

Could it be because my target in the tsconfig.json was es6 ?

yes, es6 is not supported currently, there are issues with how tsc treats Object.keys with es6

[Hanksha]

@anton-kosyakov Alright thanks!

[Hanksha]

I have to say, once I’ve got my head around how it worked the JSON-RPC stuff is really easy to use and makes backend/frontend extension communication super easy! Well done

inversifyjs and binding is tricky, but after that it is just object calls