voryx / thruway.js

RxJS WAMPv2 Client
MIT License
28 stars 13 forks source link

Connection only allows one call #10

Closed 27leaves closed 7 years ago

27leaves commented 7 years ago

Hi!

I'm still on trying to get this to run. Maybe it needs a little more documentation or it is a bug, but I currently have the problem that only the first call() will return results. Maybe you can guide me to get a proper setup? Thanks :)

My current setup:

@Injectable()
export class WampService {
  wampClient: Client;
  token: String;
  constructor(private store: Store<fromAuth.State>) {

    const url = (environment.production) ?
      'wss://' + location.host + '/router/' :
      'wss://mydevelopserver.com/router/';

      // I work with ngrx store and save the token in there if the user is logged in,
      // so I have to change the authmethods according to the state
      store
      .select(fromAuth.getUser)
      .take(1)
      .do((user) => console.log('store.select user', user))
      .subscribe(user => this.setupClient(url, user ? user.token : undefined));
  }

  setupClient (url: string, token: string) {
    let options: WampOptions = {};
    if (token) {
      options = {
        authmethods: ['ticket']
      };
    }
    this.wampClient = new Client(url, 'gateway', options);
    this.wampClient.onChallenge(function (challenge) {
      challenge.subscribe((msg) => {
    //   if (method === 'ticket') {
    //     return this.accessToken;
    //   } else {
    //     throw 'don\'t know how to authenticate via "' + method + '"';
      });
      return challenge;
    });
  }

  public call(uri: string, args?: Array<any>, argskw?: Object, options?: {}): Observable<any> {
    console.log('call', uri, args, argskw, options);
    return this.wampClient.call(uri, args, argskw, options);
  }

  public register(uri: string, callback: Function, options?: {}): Observable<any> {
    return this.wampClient.register(uri, callback, options);
  }

}
davidwdan commented 7 years ago

How are you making the calls?

27leaves commented 7 years ago

Right now, I just tried 2 things

  1. in a component with async
    
    // template
    '<div>{{ cloudVersion | async }}</div>'

// Class: cloudVersion: Observable = this.wamp .call('cloud.info.get') .do((r: EventMessage) => console.log('cloudVersion', r)) .map((r: EventMessage) => r.argskw['version']);


2. With a service
```typescript
@Injectable()
export class AuthService {
  constructor(private wampService: WampService) {}

  login({ email, password }: Authenticate) {
    return this.wampService.call('cloud.user.login', [], {
      email,
      password,
      deviceId: 'PartnerPortal',
      deviceName: 'PartnerPortal'
    });
  }
}

This gets called within an ngrx effect:

@Effect()
  login$ = this.actions$
    .ofType(Auth.LOGIN)
    .map((action: Auth.Login) => action.payload)
    .exhaustMap(auth =>
      this.authService
        .login(auth)
        .map(result => new Auth.LoginSuccess({ user: {token: result.argskw.token} }))
        .catch(error => of(new Auth.LoginFailure(error.args[0])))
    );
davidwdan commented 7 years ago

Also, I'm not sure how your authentication is working. I would do something like this:

@Injectable()
export class WampService extends Client {

    constructor() {

        const url = (environment.production) ?
            'wss://' + location.host + '/router/' :
            'wss://mydevelopserver.com/router/';

        super(url, 'gateway', {authmethods: ['ticket', 'other']});

        this.onChallenge(challenge => {

            const ticketMethod = challenge.filter((msg: ChallengeMessage) => msg.authMethod === 'ticket');
            const otherMethod = challenge.filter((msg: ChallengeMessage) => msg.authMethod === 'other');

            const userToken = store
                .select(fromAuth.getUser)
                .take(1)
                .pluck('token');

            const ticketToken = ticketMethod.flatMapTo(userToken);

            const otherToken = otherMethod.flatMap((msg: ChallengeMessage) => {
                // Do some other auth lookup
                return http.get('http://lookup_token' + msg.extra).pluck('results', 'token');
            });

            return Observable.merge(ticketToken, otherToken);
        });

    }
}
27leaves commented 7 years ago

Oh wow, that's much cleaner, thank you, I'll try to get smth like this to work :)

The thing is in our cloud implementation: When I have no token it means the user is not logged in. In this case the authmethods is empty.

davidwdan commented 7 years ago

That does complicate things a bit. What I would do is create a separate service for the wamp connection without authentication. When the user logs in, you can emit the token on a service that is a Subject:

        login({email, password}: Authenticate) {
                return this.noauthwamp.call('cloud.user.login', [], {
                    email,
                    password,
                    deviceId: 'PartnerPortal',
                    deviceName: 'PartnerPortal'
                })
                    .map(r => r.args.token)
                    .do(TokenSubjectService);
            }

And then your authentication would look like:

        this.onChallenge(challenge => {

            const ticketMethod = challenge.filter((msg: ChallengeMessage) => msg.authMethod === 'ticket');

            const localToken = store
                .select(fromAuth.getUser)
                .take(1)
                .filter(user => user)
                .pluck('token');

            // If there is a local token, that'll be used, otherwise it'll wait until TokenSubjectService reacts
            const token = Observable.race(localToken, TokenSubjectService);

            return ticketMethod.flatMapTo(token);

        });

Ideally, you'd probably want the onChallenge to trigger the login dialog, in which case you wouldn't need TokenSubjectService. Your race would look like (pseudo code):

        this.onChallenge(challenge => {

            const ticketMethod = challenge.filter((msg: ChallengeMessage) => msg.authMethod === 'ticket');

            const localToken = store
                .select(fromAuth.getUser)
                .take(1)
                .filter(user => user)
                .pluck('token');

            const remoteToken = loginDialog
                .flatMap(({email, password}) => {
                    return this.noauthwamp.call('cloud.user.login', [], {
                        email,
                        password,
                        deviceId: 'PartnerPortal',
                        deviceName: 'PartnerPortal'
                    });
                })
                .map(r => r.args.token);

            // If there is a local token, the dialog observable will be cancelled
            const token = Observable.race(localToken, remoteToken);

            return ticketMethod.flatMapTo(token);

        });
27leaves commented 7 years ago

Hi! I was able to simplify the authentication together with my backend dev, so now the auth is working!

@Injectable()
export class WampService extends Client {

  constructor(private store: Store<fromAuth.State>) {
    super(
      (environment.production) ?
        'wss://' + location.host + '/router/' :
        'wss://mydevelopserver.com/router/',
      'gateway', {
      authmethods: ['ticket']
    });

    this.onChallenge(challenge => {
      const ticketMethod = challenge.filter((msg) => msg.authMethod === 'ticket');

      const token = store
        .select(fromAuth.getUser)
        .take(1)
        .map(user => user.token);

      return ticketMethod.flatMapTo(token);
    });
  }
}

Thanks for this!

However, I'm now back to my original problem. My discoveries so far:

  1. When I want to parse the cloudVersion in the template (as above) that works, but the login doesn't work.
  2. When I remove the cloudVersion, the login call is working once. (In case of an error for example, the second call doesn't emit anything)
  3. Another page fires 3 calls at once. When I start with this page all calls work properly. Maybe it's a matter of time? Or the connection gets closed after the first messages return successfully?
davidwdan commented 7 years ago

@creat-or can you create a sample project that I can play around with?

27leaves commented 7 years ago

So, finally I got something you can hopefully work with... I created a new repository here https://github.com/creat-or/thruway-minimal-example.

There I created 2 services. WampService and WampTicketService. One is without authentication, connecting to the crossbar demo https://demo.crossbar.io/votes/index.html and the other one connects to our service via 'anonymous' ticket.

Open the console to see the logs while using the example.

Behaviour without authentication:

(Click on Banana/Chocolate/Lemon)

  1. First call: WS opens. First call returns as expected.
  2. Second Call: Another WS opens. No return.
  3. Third and further Calls: Returns as expected, but with the secondly opened WS.

Behaviour with ticket authentication:

(Click on getVersionInfo)

  1. First call: WS opens. First call returns as expected.
  2. Second Call: Another WS opens. No return.
  3. Third and further calls: No return, nothing happens.
davidwdan commented 7 years ago

@creat-or Thank you for your demo. It looks like it is a bug with thruway.js. If you make one off RPCs, the websocket connection should close when it has completed. That's not working properly. As a temporary workaround, you can hold the websocket connection open by subscribing to a topic:

    constructor() {
        super('wss://demo.crossbar.io/ws', 'crossbardemo');
        this.topic('io.crossbar.demo.keepwsopen').subscribe();
    }
27leaves commented 7 years ago

Ah nice, that works as a workaround. Thank you, too!

andzejsw commented 5 years ago
this.topic('io.crossbar.demo.keepwsopen').subscribe();

This answer is gold! Tnx!!! Fixed same issue for me too!