What I trying to do is to make the save method to wait for this.collection.create() before executing the save because otherwise it might crash.
class UserRepository extends BaseRepository<User>
{
constructor()
{
super();
this.collection = this.db.collection('users');
this.collection.create().then(res => {
if (res.code === 200)
{
// collection successfully created
}
}).catch(err => {
if (err.code === 409)
{
// collection already exists
}
});
}
}
class BaseRepository<T>
{
protected db: Database = Container.get('db');
protected collection: DocumentCollection;
public save(model: T): void
{
this.collection.save(model);
}
}
And then I can use it like this:
const userRepository = new UserRepository();
userRepository.save(new User('username', 'password'));
I can think of two solutions
Run this.collection.create() synchronously
Create a property called isCollectionReady and make a small loop in save method that wait for isCollectionReady value to change to true.
Is there any better way to do that?
Definitely don't use a loop; JavaScript is single-threaded, and the async event will never complete.
You can store off the Promise for the initialization instead and simply chain onto it:
class UserRepository extends BaseRepository<User>
{
constructor()
{
super();
this.collection = this.db.collection('users');
this._initPromise = this.collection.create().then(res => {
if (res.code === 200)
{
// collection successfully created
}
}).catch(err => {
if (err.code === 409)
{
// collection already exists
}
});
}
ensureInitialized() {
return this._initPromise;
}
}
class BaseRepository<T>
{
protected db: Database = Container.get('db');
protected collection: DocumentCollection;
public ensureInitialized() {
return Promise.resolve();
}
public save(model: T): Promise<void>
{
return this.ensureInitialized()
.then(() => this.collection.save(model));
}
}
Related
I have the following MobX class that maintain async operation:
import { makeObservable, observable, action } from "mobx";
class AsyncAction<T, P = void> {
public isLoading = false;
public error?: unknown;
public response?: T;
constructor(
private asyncAction: (payload: P) => Promise<T>,
) {
makeObservable(this, {
error: observable,
isLoading: observable,
response: observable,
setLoading: action,
setError: action,
setResponse: action,
});
}
setLoading(isLoading: boolean) {
this.isLoading = isLoading;
}
setError(error: unknown) {
this.error = error;
}
setResponse(response: T | undefined) {
this.response = response;
}
async run(payload: P) {
try {
this.setLoading(true);
const response = await this.asyncAction(payload);
this.setResponse(response);
} catch (error) {
this.setError(error);
} finally {
this.setLoading(false);
}
}
}
export { AsyncAction };
I also have the following store that extends AsyncAction:
import api from '../api';
import { AsyncAction } from "./AsyncAction";
class StatusesStore extends AsyncAction<Record<string, Status>> {
constructor() {
super(async () => {
const { statusesMap } = await api.fetchStatusesMap();
return statusesMap;
});
makeObservable(this, {
setStatus: action,
});
}
public setStatus(name: string, status: Status) {
if (this.response) {
this.response[name] = status;
}
}
}
When I trigger from my component rootStore.statusesStore.setStatus('name', 'DONE'), the component doesn't get updated.
When open devools, I see the following:
Instead of being wrapped with observable, the object keys are plain strings. This might be a reason why changing the status string triggers nothing.
How can I fix that? What am I missing?
Since async processes are resolved in the next tick of the event loop, mobx can't track the changes after the tick. One of the solutions is to use
runInAction function after every await keyword.
Like this:
import {runInAction} from 'mobx'
async run(payload: P) {
try {
this.setLoading(true);// this is okay
const response = await this.asyncAction(payload);
// everything after "await" must be wrapped
runInAction(()=>{
this.setResponse(response);
})
} catch (error) {
this.setError(error); // wrap in runInAction
} finally {
this.setLoading(false); // wrap in runInAction
}
}
There are also a few other alternatives how you can deal with promises in combination with Mobx. For me, runInAction is the most straightforward way.
For other examples check out official documentation:
Asynchronous actions
So Typescript underlines this.ratesMap.get(rate[0]) and tells it's probably undefined. Adding ! solves the problem, I don't get the error anymore but the code just won't work, the output is empty and there's no other errors in the terminal and in the console.
Why typescript underlines this.ratesMap.get(rate[0]) and not this.ratesMap.has(rate[0])? How can I fix this?
export class CardStore implements ICardStore {
public lastRates: ICard[] = [];
#observable
public cardsArray: CurrencyCard[] = []
#observable
private ratesMap: Map<string, CurrencyCard> = new Map<string, CurrencyCard>();
public constructor(#inject(CardApi) private api: CardApi) {
makeObservable(this);
this.getRates();
this.updateRate();
}
#computed
public get recentRates(): CurrencyCard[] {
return this.cardsArray = [...this.ratesMap.values()]
}
private async getRates() {
const rates = await this.api.loadRates();
runInAction(() => {
Object.entries(rates).forEach((rate) => {
if (this.ratesMap.has(rate[0])) {
this.ratesMap.get(rate[0]).update(rate[1]);
} else {
let newCard = new CurrencyCard(rate[0], rate[1]);
this.ratesMap.set(rate[0], newCard);
}
});
});
}
loadrates() from the code above is here:
public async loadRates(): Promise<Record<string, number>> {
return await fetch(
`https://freecurrencyapi.net/api/v2/latest?apikey=MYAPIKEY&base_currency=USD`
)
.then(response => response.json())
.then(data => (data.data));
}
}
The return value of the get method of Map can be undefined (if the map doesn't have an entry for that key). TypeScript doesn't know that you've guarded against that with a has call.
Instead, don't use has (doing has+get is generally an antipattern anyway [a mostly harmless one 🙂], it forces traversal of the map twice). Get the card and check if you got one:
private async getRates() {
const rates = await this.api.loadRates();
runInAction(() => {
Object.entries(rates).forEach((rate) => {
const card = this.ratesMap.get(rate[0]); // ***
if (card) { // ***
card.update(rate[1]); // ***
} else {
let newCard = new CurrencyCard(rate[0], rate[1]);
this.ratesMap.set(rate[0], newCard);
}
});
});
}
TypeScript will be able to narrow the type of card based on if (card), so it will know card is not undefined in the body of the if and will let you use update on it.
I have a MobX data store, called BaseStore that handles the status of an API request, telling the view to render when request is in progress, succeeded, or failed. My BaseStore is defined to be:
class BaseStore {
/**
* The base store for rendering the status of an API request, as well as any errors that occur in the process
*/
constructor() {
this._requestStatus = RequestStatuses.NOT_STARTED
this._apiError = new ErrorWrapper()
}
// computed values
get requestStatus() {
// if there is error message we have failed request
if (this.apiError.Error) {
return RequestStatuses.FAILED
}
// otherwise, it depends on what _requestStatus is
return this._requestStatus
}
set requestStatus(status) {
this._requestStatus = status
// if the request status is NOT a failed request, error should be blank
if (this._requestStatus !== RequestStatuses.FAILED) {
this._apiError.Error = ''
}
}
get apiError() {
// if the request status is FAILED, return the error
if (this._requestStatus === RequestStatuses.FAILED) {
return this._apiError
}
// otherwise, there is no error
return new ErrorWrapper()
}
set apiError(errorWrapper) {
// if errorWrapper has an actual Error, we have a failed request
if (errorWrapper.Error) {
this._requestStatus = RequestStatuses.FAILED
}
// set the error
this._apiError = errorWrapper
}
// actions
start = () => {
this._requestStatus = RequestStatuses.IN_PROGRESS
}
succeed = () => {
this._requestStatus = RequestStatuses.SUCCEEDED
}
failWithMessage = (error) => {
this.apiError.Error = error
}
failWithErrorWrapper = (errorWrapper) => {
this.apiError = errorWrapper
}
reset = () => {
this.requestStatus = RequestStatuses.NOT_STARTED
}
}
decorate(BaseStore, {
_requestStatus: observable,
requestStatus: computed,
_apiError: observable,
apiError: computed,
})
That store is to be extended by all stores that consume API layer objects in which all methods return promises. It would look something like this:
class AppStore extends BaseStore {
/**
* #param {APIObject} api
**/
constructor(api) {
super()
this.api = api
// setup some observable variables here
this.listOfData = []
this.data = null
// hit some initial methods of that APIObject, including the ones to get lists of data
api.loadInitialData
.then((data) => {
// request succeeded
// set the list of data
this.listOfData = data
}, (error) => {
// error happened
})
// TODO: write autorun/reaction/spy to react to promise.then callbacks being hit
}
save = () => {
// clean up the data right before we save it
this.api.save(this.data)
.then(() => {
// successful request
// change the state of the page, write this.data to this.listOfData somehow
}, (error) => {
// some error happened
})
}
decorate(AppStore, {
listOfData : observable,
})
Right now, as it stands, I'd end up having to this.succeed() manually on every Promise resolve callback, and this.failWithMessage(error.responseText) manually on every Promise reject callback, used in the store. That would quickly become a nightmare, especially for non-trivial use cases, and especially now that we have the request status concerns tightly coupled with the data-fetching itself.
Is there a way to have those actions automatically happen on the resolve/reject callbacks?
Make an abstract method that should be overridden by the subclass, and call that from the parent class. Let the method return a promise, and just hook onto that. Don't start the request in the constructor, that only leads to problems.
class BaseStore {
constructor() {
this.reset()
}
reset() {
this.requestStatus = RequestStatuses.NOT_STARTED
this.apiError = new ErrorWrapper()
}
load() {
this.requestStatus = RequestStatuses.IN_PROGRESS
this._load().then(() => {
this._requestStatus = RequestStatuses.SUCCEEDED
this._apiError.error = ''
}, error => {
this._requestStatus = RequestStatuses.FAILED
this._apiError.error = error
})
}
_load() {
throw new ReferenceError("_load() must be overwritten")
}
}
class AppStore extends BaseStore {
constructor(api) {
super()
this.api = api
this.listOfData = []
}
_load() {
return this.api.loadInitialData().then(data => {
this.listOfData = data
})
}
}
const store = new AppStore(…);
store.load();
MobX can update data that is asynchronously resolved. One of the options is to use runInAction function
example code:
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// after await, modifying state again, needs an actions:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
You can read more in official documentation: Writing asynchronous actions
Requested behaviour:
I would like to create an AngularService which checks if a certain document exists and updates a global variable based on the result.
Current State
The function checks the existence of the document successfully. It also updates the global variable in the if/else statement.
issue
Even, the first part works well, it always returns "undefined".
How can I fix that? Is it related to function scope?
My Service:
export class ProfileFollowService {
//global variable which should be updated
followState: boolean;
constructor(private angularFirestore: AngularFirestore) { }
checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
followDoc.get().then((doc) => {
if (doc.exists) {
this.followState = true;
} else {
this.followState = false;
}
});
return this.followState;
}
}
followDoc.get() is async function that returns promise. In order to return updated this.followState you have to wait for then
one way to do this is to use async / await
async checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
return followDoc.get().then((doc) => {
if (doc.exists) {
this.followState = true;
} else {
this.followState = false;
}
return this.followState;
});
}
In other part of your code where you call checksFollow you can put keyword await and wait for the response.
async someMethodToCallChecksFollow() {
const result = await this.checksFollow();
console.log(result);
}
If you want to use the response in your html, I would suggest changing followState from primitive boolean to BehaviorSubject<boolean> and then call this.followState.next(true)
For example:
export class YourService {
public followState = new BehaviorSubject<boolean>(false);
async checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
return followDoc.get().then((doc) => {
if (doc.exists) {
this.followState.next(true);
} else {
this.followState.next(false);
}
return this.followState.getValue();
});
}
}
And then in your html you can use async pipe.
<div *ngIf="yourServiceInstance.followState | async">It is true</div>
On my component, I get some translations from my service.
The service makes a call to an MVC API controller.
The code on the component:
private getTranslations(): void {
this._translationService.getTranslations('Foo');
}
The code on the service:
public translations: Translation[] = new Array<Translation>();
public getTranslations(action: string) {
this._http.get<Translation[]>(this.baseUrl + action).subscribe(
(result: Translation[]) => {
result.forEach(element => {
this.translations.push(element);
});
},
(error: any) => this._loggerService.logError(error)
);
}
In the service, it sets a value on the variable this.translations in a subscription.
How can I wait in my component for this to "complete" meaning that the public variable this.translations is set?
I saw a thread to wrap it in a new Promise() but I wasn't able to figure it out in my example. Any method can be used.
You can return an observable from your service:
public translations: Translation[] = new Array<Translation>();
public getTranslations(action: string) {
return new Observable((observer) => { // <-- return an observable
this._http.get<Translation[]>(this.baseUrl + action).subscribe(
(result: Translation[]) => {
result.forEach(element => {
this.translations.push(element);
});
observer.complete(); // <-- indicate success
},
(error: any) => {
this._loggerService.logError(error);
observer.error(error); // <-- indicate error
}
);
});
}
and subscribe in the component:
private getTranslations(): void {
this._translationService.getTranslations('Foo').subscribe({
error: () => { /* handle error */ },
complete: () => { /* handle complete */ },
});
}