RxJs: How to conditionally chain observable of BehaviorSubject? - conditional-statements

I've got an observable data service (UserService) that returns the currently logged in user. I followed this tutorial - https://coryrylan.com/blog/angular-observable-data-services, which describes using a BehaviorSubject to return a default currentUser immediately, then emit the real currentUser once it's loaded or altered. The service is basically like this...
private _currentUser: BehaviorSubject<User> = new BehaviorSubject(new User());
public currentUser: Observable<User> = this._currentUser.asObservable();
constructor(private http: Http) {}
loadUser() { // app.component onInit and login component call this
return this.http.get('someapi.com/getcurrentuser')
.map(response => <User>this.extractData(response))
.do(
(user) => {
this.dataStore.currentUser = user;
this._currentUser.next(Object.assign(new User(), this.dataStore).currentUser);
},
(error) => this.handleError(error)
)
.catch(error -> this.handleError(error));
}
I'm having problems whenever a user hits F5 to reload the entire spa. When a consuming component subscribes to the currentUser on the UserService, it immediately receives a default user while the UserService waits for an api call to receive the actual user. The moment that api call finishes, the real user is emitted by UserService and all the subscribers get the real user. The first value emitted by the BehaviorSubject, however, is the default value and it always has an id of "undefined", so we can't make our next api call yet. In fact, when the real user comes through and I CAN make a valid call using the user.id, the chained subscription never happens and I don't get the values out of the response.
I know I'm doing something stupid, but I haven't figured out exactly what yet. I just stumbled across concatMap, but I'm not sure how to use it. While I pursue that, I'd like to know why the below code doesn't work. I particularly want to know why the subscribe never fires, even after the real user comes in, just to help my newbie understanding of Observables.
this.userService.currentUser
.flatMap((user) => {
this.user = user;
// Need to NOT call this if the user does not have an id!!!
this.someOtherService.getSomethingElse(user.id); // user.id is always undefined the first time
})
.subscribe((somethingElse) => {
// This never gets called, even after the real user is emitted by the UserService
// and I see the getSomethingElse() call above get executed with a valid user.id
this.somethingElse = somethingElse;
});

If you want to ignore user instances that do not have an id, use the filter operator:
import 'rxjs/add/operator/filter';
this.userService.currentUser
.filter((user) => Boolean(user.id))
.flatMap((user) => {
this.user = user;
this.someOtherService.getSomethingElse(user.id);
})
.subscribe((somethingElse) => {
this.somethingElse = somethingElse;
});
Regarding "why the subscribe never fires", it's likely due to an error arising from the undefined id. You only pass a next function to subscribe, so any errors will be unhandled. And if an error occurs, the observable will terminate and will unsubscribe any subscribers - as that is how observables behave - so any subsequent users with defined id properties will not be received.

Related

Show private messages list of multiple users logged in at the same time

I've created a private messaging app where users login and can view their private messages. The problem that I am having is that is only shows the message list of one logged in user at a time. So let's say User A is logged in, it will show the chat list of user A. But then User B logs in, then both User A and User B will see the chat list of User B.
This is my server side call to fetch chats by user id:
Im using express for the backend
io.on('connection', socket => {
socket.on('findAllChatsByUserId', (userId) => {
socket.userId = userId
socket.join(socket.userId)
ChatModel.aggregate([{$match: {$or:[{senderId: userId},{receiver: userId}]}}, {$group: {_id: '$chatId', 'data': {$last: '$$ROOT'}}}]).exec(function(error, data) {
if (error) {
return error
} else {
data.sort(function (a, b) {
return b.data.date - a.data.date;
});
io.to(socket.userId).emit('findAllChatsByUserId', data);
}
})
})
});
And on the client side I do:
I am using VueJs on the FE
mounted () {
this.loading = true
this.socket.emit('findAllChatsByUserId', this.getUserId) // this calls the socket to get the chats for the given user Id
this.loading = false
},
I tried creating rooms by userId to make sure that only the data for a given user ID is passed in but it seems like only one user can use the socket at a time. I thought the rooms would solve this issue for me. Do I have to create a separate socket for each user? If so, how do I do that? I've followed the socket.io private messaging tutorial but they use 2 users talking to each other to explain the problem.
So I ended up solving this by doing:
io.to(socket.id).emit('findAllChatsByUserId', data);
instead of:
io.to(socket.userId).emit('findAllChatsByUserId', data);
So you use the "to" attribute to make sure the data you're sending is going to a particular socket, and you can find your specific socket by just calling socket.id (you don't have to set this, it gets set on its own. And the data will get emitted to whomever is on that specific socket.

How to unregister middleware in Telegraf?

When I add bot.hears(...), it registers middleware for handling matching text messages. But now it will handle those messages even if they are sent any time, even if not expected.
So if I am creating a stateful service, I would like to listen to particular messages only at appropriate time.
How can I unregister middleware, so that it does not hear any more previously handled messages?
I turned out I was looking for Scenes. How to use them is described on Github.
I'll just post a slightly modified code from the links above:
const { Telegraf, Scenes, session } = require('telegraf')
const contactDataWizard = new Scenes.WizardScene(
'CONTACT_DATA_WIZARD_SCENE_ID', // first argument is Scene_ID, same as for BaseScene
(ctx) => {
ctx.reply('Please enter guest\'s first name', Markup.removeKeyboard());
ctx.wizard.state.contactData = {};
return ctx.wizard.next();
},
(ctx) => {
// validation example
if (ctx.message.text.length < 2) {
ctx.reply('Please enter real name');
return;
}
ctx.wizard.state.contactData.firstName = ctx.message.text;
ctx.reply('And last name...');
return ctx.wizard.next();
},
);
const stage = new Scenes.Stage();
stage.register(contactDataWizard);
bot.use(session());
bot.use(stage.middleware());
But I still don't know how to generally implement it, so I need to find it out in the Scenes code of Telegraf.

Correct usage of getters in vuex

I'm currently developing an app with vue v2, and something got me thinking. It's kind of hard to find information for good practices so I decided I will ask here.
I have a laravel api and vue for front end. Until now my models were such that will hold only a few objects (< 100), so I just put the in the state, and get what ever needed with a getter. Now I have a model that will hold > 10000 objects, so putting them all into the state is pretty much out the topic. The way I solve this problem is a have with action that gets a single object from the api by id, and put it in the state, after a check if its not in there already. The check happens with a getter that I'm also using when I need this object. So I have similar structure:
​
// mutation
[mutation_types.FETCH_SOMETHING](state, {something}) {
if (!this.getters.getSomethingById(something.id)) {
state.somethings.push(something)
}
}
// action
const {data} = await axios.get('~route~')
commit(mutation_types.FETCH_SOMETHING, {something: data.something})
// getter
getSomethingById: state => id => {
return state.somethings.find(something => something.id === id)
}
​
So this is working, but there are 2 problems that I see. The first is every time when I need a something I should call the dispatch to get the item from the api into the state and then the getter to get the actual object and the second problem is the check is happening after the api call, so even if I have this specific something in the state, I'm doing a request for it.
A way to fix both of those problems that I though of calling the just working with the getter, which will check that state, if the searched ID is found in there it will be returned, if not, the dispatch will get it from the api and then return it.
I feel like I'm adding logic to the getter that doesn't belong there, but I can't think of another way to achieve this. Is there something that I'm missing / doing wrong somewhere?
Take advantage of the async nature of vuex actions and promises. Do the check for existence directly in the action along with the api call if needed:
state: {
items: []
}
mutations: {
addItem: (state, item) => {
state.items.push(item)
}
}
actions: {
fetchItem: async (context, id) => {
let item = context.state.items.find(i => i.id === id)
if (!item) {
item = await axios.get('/example/' + id).then(response => {
context.state.commit('addItem', response.data)
return response.data
})
}
return item
}
}

Accessing information of authenticated user in request scope using express

I'm using Express with TypeORM and currently I got stuck in the following situation:
Before inserting or updating a record, I always want to set the username to the current record to persist the information who last updated this record.
With TypeORM, this can easily be done by using subscribers:
#EventSubscriber()
export class Subscriber implements EntitySubscriberInterface {
beforeUpdate(event: UpdateEvent<any>) {
const entity = event.entity;
if(entity instanceof Base) {
entity.lastUpdatedBy = 'username'; // TODO
}
}
}
The user is known after he was successfully authenticated:
#Put('/entity')
public updateEntity(#CurrentUser({required: true}) user: User, #Body enity: Entity): Promise<Entity> {
Log.info('User successfully authenticated: ' + user)
return this.manager.update(entity);
}
My question is: What is the simpliest way to store and access user information in a request scope?
What I found out so far:
The following stackoverflow-post seems to be related, but the
solutions sounds like a overhead for me (GLOBAL data per HTTP/Session request?)
I also could append the user information in the updateEntity method, but then I have to explicitly all the time.
Is there any other option?
You can use the atttribute data of type any of the SaveOptions.
#Put('/entity')
public updateEntity(#CurrentUser({required: true}) user: User, #Body enity: Entity): Promise<Entity> {
Log.info('User successfully authenticated: ' + user)
return this.manager.update(entity, {data: user});
}
And then, you can use the data in Subscriber with:
event.queryRunner.data
I used the following npm package to save the user in my request scope: https://www.npmjs.com/package/cls-hooked.
This package did not work for me since async/await seems not to be supported: continuation-local-storage.

Meteor.loginWithPassword callback doesn't provide custom object in User accounts doc

Meteors loginWithPassword() function doesn't provide me the object systemData, which I adding to user doc (not to profile obj) during registration. The thing is, that if I look into console after logging in, I can see that object systemData (that means probably it's not publish issue), but not in callback of loginWithPassword() function, where I need them (to dynamically redirect user to proper page). Is there way to get this object, without any ugly things like timers?
Meteor.loginWithPassword(email, password, function(errorObject) {
if (errorObject) {
...
} else {
// returns true
if (Meteor.userId()) {
// returns false
if (Meteor.user().systemData) {
...
}
// user doc without systemData object
console.log(JSON.stringify(Meteor.user());
}
}
I've adding object systemData on creating user:
Accounts.onCreateUser(function(options, user) {
if (options.profile) {
user.profile = options.profile;
}
...
user.systemData = systemDataRegularUser;
return user;
});
Are you sure publish data to Client ?
I get User Info Using loginWithPassword in callback function.
Meteor.loginWithPassword username,password,(error,result1)->
options =
username: username
password: password
email: result['data']['email']
profile:
name: result['data']['display-name']
roles: result.roles
console.log Meteor.user(), result1
I Create user flowing code: (options contains systemData)
Accounts.createUser option
The first problem is that you want a custom field on a user document published to the client. This is a common question - see the answer here.
The next problem is that even after you add something like:
Meteor.publish("userData", function () {
return Meteor.users.find(this.userId, {fields: {systemData: 1}});
});
I think you still have a race condition. When you call loginWithPassword, the server will publish your user document, but it will also publish another version of the same document with the systemData field. You are hoping that both events have completed by the time Meteor.user() is called. In practice this may just work, but I'm not sure there is any guarantee that it always will. As you suggested, if you added a slight delay with a timer that would probably work but it's an ugly hack.
Alternatively, can you just add systemData to the user's profile so it will always be published?
I didn't find exact way how to solve this, but found easy workaround.
To make some action right after user logged in (eg. dynamically redirect user to proper page), you can hook on your home page with Iron router.(If you using it.) :
this.route('UsersListingHome', {
path: '/',
template: 'UsersListing',
data: function() { ... },
before: function() {
if (isCurrentUserAdmin() && Session.get('adminJustLogged') !== 'loggedIn') {
Router.go('/page-to-redirect');
Session.set('adminJustLogged','loggedIn');
}
}
});
After click on logout of course if (isCurrentUserAdmin()) { Session.set('adminJustLogged', null); }
I've further thought about calling Meteor.call('someMethod') to fetch userData object in Method callback, but now I'm satisfied.
PS: I know that it's not recommended to have plenty session variables or other reactive data source for speeding-up my app, but I believe, that one is not tragedy :)
PS2: Anyway, thanks for your answers.