I have an Angular application using AngularFire, NgRx and Cloud Firestore as db, where I enabled offline persistence.
My problem is that when I change a document while offline the effect function does not trigger the success action, as Firestore promises are not resolved while offline, but only after the request reaches the server.
At the moment I am stuck in trying to find a good way to update the store with the local data when offline.
One idea could be to check the fromCache flag before loading the data, so that if fromCache is true (that is, we are offline) I can load the data from the local db instead of the store, but it looks to me like a dirty workaround.
Effect
//--- Effect triggered to load document in the home page ----
firstSet$ = createEffect(() => {
return this.actions$.pipe(
ofType(PlacesActions.loadFirstPlaces),
switchMap(() => {
return this.placeService.getFirstSet().pipe(
map((places) => {
return PlacesActions.loadFirstPlacesSuccess({ places });
}),
catchError((error) => of(PlacesActions.loadFirstPlacesFailure({ error }))),
takeUntil(this.subService.unsubscribe$)
);
})
);
});
//--- Effect triggered when a document is updated ----
updatePlace$ = createEffect(() => {
return this.actions$.pipe(
ofType(PlacesActions.updatePlace),
concatMap((action) => {
// ----- Below the NEW code, without promise ----
try {
this.placeService.savePlace(action.place);
this.router.navigate(['/home']);
return of(PlacesActions.updatePlaceSuccess({ place: action.place }));
}
catch(error) {
return of(PlacesActions.updatePlaceFailure({ error }))
}
/*
// ----- Below the old code ----
return from(this.placeService.savePlace(action.place))
.pipe(
map((place: Place) => {
this.router.navigate(['/home']);
return PlacesActions.updatePlaceSuccess({ place });
}),
catchError((error) => of(PlacesActions.updatePlaceFailure({ error })))
);
*/
})
);
});
DB service
savePlace(place): void {
this.firestoreRef.doc<Place>(`places/${place.id}`).update(place);
}
/* Old version of savePlace using Promise for the Update
async savePlace(place): Promise<void> {
return await this.firestoreRef.doc<Place>(`places/${place.id}`).update(place);
}
*/
loadFirstPlaces(limit: number = 9,
orderBy: OrderModel = { propName: 'modifiedOn', order: 'desc' }){
const query = (ref: CollectionReference) =>
ref.orderBy(orderBy.propName, orderBy.order)
.limit(limit);
return this.firestoreRef.collection<Place>('Places', query)
.valueChanges()
.pipe(shareReplay(1));
}
Home component
ngOnInit(): void {
// Set the data from the store, if available there
this.places$ = this.store.select(getAllPlaces).pipe(
tap((data) => {
this.places = data;
})
);
/*Dispatch load action to load data from the server or local cache.
If I use the PROMISE approach for the update method,
the data coming from the local cache has the old data.
The edits done while offline are not provided.*/
this.store.dispatch(loadFirstPlaces());
}
The local cache has been updated when the update() call completes. So that's the right moment to update the state of your application too:
async savePlace(place): Promise<void> {
const result = this.firestoreRef.doc<T>(`places/${place.id}`).update(place)
// TODO: update the state here
return await result;
}
I'm not even sure if you should be returning a promise here. If savePlace is meant to return whether the local operation was successful, it should simply be:
savePlace(place): void {
this.firestoreRef.doc<T>(`places/${place.id}`).update(place)
}
If the local write operation fails, update will throw an exception and that will escape from savePlace to signal failure to the caller.
Related
I have two APIs reporting two sets of data (lockboxes and workstations). The lockboxes API has a collection of agencies with a recordId that I need to manipulate. The workstations API is the main collection that will assign one of these agencies (lockboxes) on a toggle to a workstation by sending the lockboxes.recordId and the workstation.recordId in the body to the backend.
My store looks like this
import { axiosInstance } from "boot/axios";
export default {
state: {
lockboxes: [],
workstation: []
},
getters: {
allLockboxes: state => {
return state.lockboxes;
},
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
},
actions: {
async fetchLockboxes({ commit }) {
const response = await axiosInstance.get("agency/subagency");
commit("setLockboxes", response.data.data);
},
updateAgency: ({ commit, state }, { workstation, lockboxes }) => {
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
}
},
mutations: {
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
setLockboxes: (state, lockboxes) => (state.lockboxes = lockboxes)
}
};
Process:
When I select a lockbox from the dropdown and select a toggle switch in the workstation that I want to assign the lockbox too, I do get the lockbox to show but it goes away on refresh because the change only happened on the front end. I'm not really passing the workstation.recordId or lockboxes.recordId in my body as I hoped I was. It is not reading the state and recognizing the recordId for either state(workstation or lockboxes).
the console.log is returning (Uncaught (in promise) undefined)
The request is 404ing with an empty Payload in the body ( {} )
Not even the mutation is firing
template
toggleAssign(workstation) {
this.updateAgency(workstation);
}
At some point I had it that is was reading the workstation.recordId before I tried to merge the two states in the getter but I was never able to access the lockboxes.recordId. How can I have access to two states that live in two independent APIs so I can pass those values in the body of the request?
You can add debugger; in your code instead of console.log to create a breakpoint, and inspect everything in your browser's debug tools.
I can't really help because there are very confusing things:
state: {
lockboxes: [],
workstation: []
},
So both are arrays.
But then:
setWorkstation: (state, workstation) => (state.workstation = workstation),
assignAgency(workstation) { workstation.assign = !workstation.assign},
It seems that workstation is not an array?
And also this, in the getters:
singleWorkstation: state => {
let result = {
...state.workstation,
...state.lockboxes
};
return result;
}
I'm not understanding this. You're creating an object by ...ing arrays? Maybe you meant to do something like:
singleWorkstation: state => {
let result = {
...state.workstation,
lockboxes: [...state.lockboxes]
};
return result;
}
Unless lockboxes is not an array? But it's named like an array, it's declared as an array. You do have this however:
const postdata = {
recordId: state.workstation.recordId,
agency: state.lockboxes.recordId
};
So it seems it's not an array?
Finally, in your updageAgency method, and this is where the problem may lie:
return new Promise((resolve, reject) => {
axiosInstance
.post("Workstation/update", postdata)
.then(({ data, status }) => {
if (status === 200) {
resolve(true);
commit("setWorkstation", data.data);
commit("assignAgency", workstation);
console.log(state);
}
})
.catch(({ error }) => {
reject(error);
});
});
The .then first arg of axios is only invoked if the status code is 2xx or 3xx. So your test if (status === 200) is superfluous because errors would not get there. And if for a reason of another you have a valid code other than 200, the promise never ends. reject is never called, as it's not an error, and neither is resolve. So you should remove check on the status code.
You should also call resolve(true) after the two commits, not before...
Finally your mutation assignAgency is declared all wrong:
assignAgency(workstation) { workstation.assign = !workstation.assign},
A mutation always takes the state as the first param. So it should either be:
assignAgency(state, workstation) {
state.workstation = {...workstation, assign: !workstation.assign}
},
Or
assignAgency(state) {
state.workstation = {...state.workstation, assign: !state.workstation.assign}
},
Depending on if you even need the workstation argument, given that what you want is just toggle a boolean inside an object.
TLDR: I'm not sure if lockboxes should be an array or an object, remove the status check inside your axios callback, fix the assignAgency mutation, use breakpoints with debugger; and the VueJS chrome plugin to help examine your store during development.
In an action, you get passed 2 objects
async myAction(store, payload)
the store object is the whole vuex store as it is right now. So where you are getting commit, you can get the state like so
async fetchLockboxes({ commit,state }) {//...}
Then you can access all state in the app.
You may use rootState to get/set whole state.
updateAgency: ({ commit, rootState , state }, { workstation, lockboxes }) {
rootState.lockboxes=[anything you can set ]
}
I thought I had a simple function:
database change trigger (.onUpdate)
find out which change is possibly important for a notification (prepareNotifications(change))
ask firebase if there are records that want a notification about that change (getDatabaseNotifications(changeWithNotification))
sent notifications (sentNotifications(changeWithNotification))
I'm stuck for a couple off days now on how to resolve the Firebase call before moving on.
tried to Promise.all([getDatabaseNotifications()])
tried to chain this function like this:
changes
then firebase call
then sent notifiactions
What is happening:
I get the changes,
The call to Firebase is done,
But before waiting for the result it moves on to sending notifications.
It finds no notifications in Firebase (but there are notifications!)
It's gathering the notifications (array [])
... here I push a test notification ...
It's sending 1 notification (the test notification)
Then it resolves the Firebase notifications (this should be resolved before).
Then the function stops without doing anything anymore.
This is how my function looks now. Can someone explain how I can wait on Firebase?
exports.showUpdate = functions.firestore
.document('shows/{showId}')
.onUpdate((change, context) => {
return prepareNotifications(change) // check if and which notifications to get out of the firebase database
.then(changes => {
console.log('changes');
console.log(changes);
if(changes) {
const gatherData = [];
changes.forEach(change => {
console.log('change');
console.log(change);
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
.catch(err => {
console.log(err);
})
})
return gatherData;
}
return null;
})
.then(notifications => {
console.log('notifications');
console.log(notifications);
notifications.push(testData); // add some test notifications
if (notifications && notifications.length > 0) {
sentNotifications(notifications); // sent notifications
return 'sending notifications';
}
return 'no notifications to sent';
})
.catch(err => {
Sentry.captureException(new Error(`Showupdate sending notifications not ok. Error message: ${err.message}`));
})
});
Updated code which works! thanks to your examples.
exports.showUpdate = functions.firestore
.document('shows/{showId}')
.onUpdate((change, context) => {
return prepareNotifications(change) // check if and which notifications to get out of the firebase database
.then(changes => {
if(changes) {
return getDbRecords(changes);
}
})
.then(notifications => {
if (notifications && notifications.length > 0) {
sentNotifications(notifications); // sent notifications
return 'sending notifications';
}
return 'no notifications to sent';
})
.catch(err => {
Sentry.captureException(new Error(`Showupdate sending notifications not ok. Error message: ${err.message}`));
})
});
function getDbRecords(changes) {
const gatherData = [];
const gatherDataPromises = [];
changes.forEach(change => {
gatherDataPromises.push(
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push(...data);
})
);
});
return Promise.all(gatherDataPromises)
.then(() => { return gatherData }
);
}
This section of your code doesn't handle promises properly, it creates a bunch of work but then will return gatherData before any of it has happened, which is why you don't see any notifications:
if(changes) {
const gatherData = [];
changes.forEach(change => {
console.log('change');
console.log(change);
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
.catch(err => {
console.log(err);
})
})
return gatherData;
}
Notably, you probably want that return gatherData to be chained off the set of promises that are generated by the entire set of calls to getDatabaseNotifications.
Something like:
if(changes) {
const gatherData = [];
const gatherDataPromises = [];
changes.forEach(change => {
console.log('change');
console.log(change);
gatherDataPromises.push(
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
);
});
return Promise.all(gatherDataPromises)
.then(() => { return gatherData });
}
I removed the catch statement to allow the error to bubble up to the top level catch.
Caution: I have not tested this, as I don't have sample data or the code for getDatabaseNotifications, but the general approach should solve your current problem. Likewise, it allows all the calls to getDatabaseNotifications to run in parallel, which should be significantly faster than just awaiting on them in sequence.
That said, you do have other problems in this code -- for example, the return null just below the block I am discussing will likely lead you into trouble when you try to use notifications.push() in the following then() (but this also appears to be test code).
I think it's because of the async nature of the methods. So, instead of waiting "getDatabaseNotifications()" to finish it's job, it jumps into ".then(notifications =>{}" and in this case gatherData returns empty.
putting await before calling the method might work.
await getDatabaseNotifications(change.showId, change.ring, change.ringNumber)
I'm building a PWA using the Vue CLI 3 PWA plugin, and I'm not sure how to handle the response from an AJAX call made while offline.
All I have is basically sample code from the Workbox documentation...
// service-worker.js
const queue = new workbox.backgroundSync.Queue('CR_OfflineQueue')
self.addEventListener('fetch', (event) => {
const promiseChain = fetch(event.request.clone())
.catch((err) => {
console.log('Queued Event:', event.request)
return queue.addRequest(event.request)
})
event.waitUntil(promiseChain)
})
The AJAX is just a basic Axios get() that's called when the store is loaded and in a test mutation
// store.js
getUsername () {
return axios.get(
'http://localhost:8005/username/',
{
responseType: 'json',
withCredentials: true
})
}
const store = new Vuex.Store({
state: {
username: null
},
mutations: {
test_api (state) {
getUsername()
.then((res) => {
state.username = res.username
})
.catch((err) => {
console.error('getUsername:', err)
})
}
}
})
The request is being successfully replayed, but I have no idea what to do with it, and I'm unable to find an example using workbox.backgroundSync.Queue.
As far as getUsername() is concerned, the request failed, but how do I have the calling function essentially pick up where it left off?
I don't think that need something to check the queue for the request at a regular interval to "manually" re-trigger (not via the auto replay). How would I use the queue class to take the result from the replayed request and set the username property?
In the example provided in the aor-realtime readme
import realtimeSaga from 'aor-realtime';
const observeRequest = (fetchType, resource, params) => {
// Use your apollo client methods here or sockets or whatever else including the following very naive polling mechanism
return {
subscribe(observer) {
const intervalId = setInterval(() => {
fetchData(fetchType, resource, params)
.then(results => observer.next(results)) // New data received, notify the observer
.catch(error => observer.error(error)); // Ouch, an error occured, notify the observer
}, 5000);
const subscription = {
unsubscribe() {
// Clean up after ourselves
clearInterval(intervalId);
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
const customSaga = realtimeSaga(observeRequest);
fetchData function is mentioned, but it's not accessible from that scope, is it just a symbolic/abstract call ?
If I really wanted to refresh the data based on some realtime trigger how could i dispatch the data fetching command from this scope ?
You're right, it is a symbolic/abstract call. It's up to you to implement observeRequest, by initializing it with your restClient for example and calling the client methods accordingly using the fetchType, resource and params parameters.
We currently only use this saga with the aor-simple-graphql-client client
I'm working on an app which has to manage a large amount of data.
In the init process several api calls must be done while the user sees a loading bar.
Here is my init action:
export function init(key) {
return (dispatch, getState) => {
// start init
dispatch(initStart());
setTimeout(() => {
dispatch(initProcess(10));
}, 0)
return Promise.all([
// load users
dispatch(loadUsers(key)).then(() => {
dispatch(initProcess(30));
}),
// load other stuff
// ...
// load articles
dispatch(loadArticles(key)).then(() => {
dispatch(initProcess(60));
}),
// should be executed after all actions/reducers are done
]).then(() => {
setTimeout(() => {
dispatch(initFinish());
}, 700);
});
}
}
So far it works perfectly, but there will be 20k to 50k articles. The backend has to perform some joins to get the data together, so I'm sure I'll get a server timeout if I try to get them in one piece.
The idea is to fetch the total number first and then get the articles in 1k pieces in a loop. But it wont work the way I need it. I'm getting initFinish dispatched after the articles are counted but not after they are fetched.
Here is the loadArticles action:
export function loadArticles(key) {
return (dispatch, getState) => {
// check local db first
// get total number
return dispatch(countArticles(key))
.then(result => {
Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
}
}
I have no loop yet but thats not the point. The logic remains the same. I return the dispatch(countArticles(key)) before fetchArticles is done.
Has anyone a hint? That would be awesome.
EDIT
coutArticles and fetchArticles
function countArticles(key) {
return {
[CALL_API]: {
types: [ COUNT_ARTICLES_REQUEST, COUNT_ARTICLES_SUCCESS, COUNT_ARTICLES_FAILURE ],
endpoint: `articles`,
schema: Schemas.ARTICLE_COUNTER
}
}
}
function fetchArticles(key, take, skip) {
return {
[CALL_API]: {
types: [ FETCH_ARTICLES_REQUEST, FETCH_ARTICLES_SUCCESS, FETCH_ARTICLES_FAILURE ],
endpoint: `articles/${take}/${skip}`,
schema: Schemas.ARTICLE_ARRAY
}
}
}
The middleware is the same es here
2. EDIT
if i change
// get total number
return dispatch(countArticles(key))
.then(result => {
Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
to
// get total number
dispatch(countArticles(key))
.then(result => {
return Promise.all([
// No idea how to put the loop in here :(
dispatch(fetchArticles(key, 1000)),
])
});
I get Uncaught TypeError: Cannot read property 'then' of undefined on dispatch(loadArticles(key)).
3. EDIT
Some days later I'm still fighting ^^
Here is the simplified init function, which should (only) get the count result and then fetch the articles:
But for now im failing already here:
export function init(key) {
return (dispatch, getState) => {
countArticles(key).then(result => {
console.log(result);
});
}
}
Output:
Uncaught TypeError: countArticles(...).then is not a function
I had problems with chaining dispatch as well; it should return a Promise but I could not get it to work.
So I would change the logic this way
countArticles(key).then(result => {
dispatch( {
type: RECEIVED_NUM_ARTICLES,
value: result
})
Promise.all([...Array(Math.floor(result/1000))].map(_ =>
fetchArticles(key,1000).then(res => dispatch( {
type: RECEIVED_1000_ARTICLES,
value: res
})
)).then( _ => dispatch({
type: RECEIVED_EVERYTHING
value: 'whatever'
})
)
)
(I did not test this code)
But basically: fetch then dispatch the result, then chain another fetch/dispatch sequence, etc...
The loop needs rework to fetch the modulo
The advantage with this approach is that you can update the UI with the number of articles when you have them, then provide updates on every 1000 articles fetched and finally notify when done