How to create streaming endpoint without initial data fetch in redux toolkit - javascript

I need to use a streaming connection in redux-toolkit.
The example in docs is implemented using an initial fetch to retrieve the previous message history:
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getMessages: build.query<Message[], Channel>({
// retrieve initial data
query: (channel) => `messages/${channel}`,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
// create a websocket connection when the cache subscription starts
const ws = new WebSocket('ws://localhost:8080')
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded
// when data is received from the socket connection to the server,
// if it is a message and for the appropriate channel,
// update our query result with the received message
const listener = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (!isMessage(data) || data.channel !== arg) return
updateCachedData((draft) => {
draft.push(data)
})
}
ws.addEventListener('message', listener)
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await cacheEntryRemoved
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
ws.close()
},
}),
}),
})
export const { useGetMessagesQuery } = api
My endpoint only admits streaming updates, but doesnt have an initial data response. How can I implement the streaming endpoint without the initial fetch?
What I have tried:
Removing the query property.
Returning undefined in the responseHandler of query.

One option here would be to use a queryFn that returns an initial empty data value (and thus doesn't make an API call to the server), such as:
streamSomeData: build.query({
queryFn: async () => ({data: null}),
onCacheEntryAdded: async (
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) => {
// streaming logic here
}
})

Related

React Prop returning Null as it relies on state

Hopefully a simply one.
I make an API call in my component which brings down some account information such as AccountUid, Category etc, i use state to set these.
useEffect(() => {
fetch(feed_url, {
headers: {
//Headers for avoiding CORS Error and Auth Token in a secure payload
"Access-Control-Allow-Origin": "*",
Authorization: process.env.REACT_APP_AUTH_TOKEN,
},
})
//Return JSON if the Response is recieved
.then((response) => {
if (response.ok) {
return response.json();
}
throw response;
})
//Set the Account Name state to the JSON data recieved
.then((accountDetails) => {
setAccountDetails(accountDetails);
console.log(accountDetails.accounts[0].accountUid);
console.log(accountDetails.accounts[0].defaultCategory);
})
//Log and Error Message if there is an issue in the Request
.catch((error) => {
console.error("Error fetching Transaction data: ", error);
});
}, [feed_url]);
This Works perfectly well and it Logs the correct values in my .then when testing it.
The issue however is that i want to pass these down as props. But i get an error that they are being returned as null (My default state).. i presume as they're jumping ahead.
<div className="App">
<GetAccountName
accountUID={accountDetails.accounts[0].accountUID}
defCategory={accountDetails.accounts[0].defaultCategory}
/>
</div>
How do i pass the the 2 details im logging as props?? I've tried setting default state to "" instead of null and just get that it is undefined.
If you dont want to use conditional render in your child component, so you should try optional chaining
<GetAccountName
accountUID={accountDetails?.accounts?.[0]?.accountUID}
defCategory={accountDetails?.accounts?.[0]?.defaultCategory}
/>
Since fetching is asyncronous, the most common way is to show some loading indicator (like a spinner) & once the data come in, show the component instead.
If you don't need an indicator, you might just return null.
The general idea is to manipulate some intermediary states (e.g. data, isError) based on the promise state.
Check out react-query library example or a lighter abstraction like useFetch hook to see how they manage it.
Here's a sample implementation of useFetch taken from this article:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState(() => {});
React.useEffect(() => {
const fetchData = async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, {...options, signal});
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
}
}, []);
return { response, error, abort };
};

What is best practice to remove duplicate/redundant API requests?

So I am working in a React platform that has data that updates every second(I would like to move to web-sockets but its currently only supports gets).Currently, each component makes a fetch request for itself to get the data for the widget. Because the fetch requests are built into the widgets there are redundant api requests for the same data. I am looking for a possible better solution to remove these redundant api requests.
The solution I came up with uses what I call a data service that checks for any subscription to data sources then makes those api calls and places the data in a redux state for the components to then be used. I am unsure if this is the best way to go about handling the issue I am trying to avoid. I don't like how I need an interval to be run every second the app is running to check if there are "subscriptions". I am unsure if thats the correct way to go about it. With this solution I don't duplicate any requests and can add or remove a subscription without affecting other components.
One more thing, the id can change and will change what data I recieve
Here is a simplified version of how I am handling the service.
const reduxState = {
id: "specific-id",
subscriptions: {
sourceOne: ["source-1-id-1", "source-1-id-2", "source-1-id-3"],
sourceTwo: ["source-2-id-1", "source-one-id-2"],
},
data: {
sourceOne: { id: "specific-id", time: "milliseconds", data: "apidata" },
sourceTwo: { id: "specific-id", time: "milliseconds", data: "apidata" },
},
};
const getState = () => reduxState; //Would be a dispatch to always get current redux state
const dataService = () => {
const interval = setInterval(() => {
const state = getState();
if (state.subscriptions.sourceOne.length > 0)
fetchSourcOneAndStoreInRedux();
if (state.subscriptions.sourceTwo.length > 0)
fetchSourceTwoAndStoreInRedux();
}, 1000);
};
const fetchSourcOneAndStoreInRedux = (id) =>{
return async dispatch => {
try {
const res = await axios.get(`/data/one/${id}`)
dispatch(setSourceOneDataRedux(res.data))
} catch (err) {
console.error(err)
}
}
}
I am building my components to only show data from the correct id.
Here is a simple working example of a simple "DataManager" that would achieve what you are looking for.
class DataManager {
constructor(config = {}) {
this.config = config;
console.log(`DataManager: Endpoint "${this.config.endpoint}" initialized.`);
if (this.config.autostart) { // Autostart the manager if autostart property is true
this.start();
}
}
config; // The config object passed to the constructor when initialized
fetchInterval; // The reference to the interval function that fetches the data
data; // Make sure you make this state object observable via MOBX, Redux etc so your component will re-render when data changes.
fetching = false; // Boolean indicating if the APIManager is in the process of fetching data (prevent overlapping requests if response is slow from server)
// Can be used to update the frequency the data is being fetched after the class has been instantiated
// If interval already has been started, stop it and update it with the new interval frequency and start the interval again
updateInterval = (ms) => {
if (this.fetchInterval) {
this.stop();
console.log(`DataManager: Updating interval to ${ms} for endpoint ${this.config.endpoint}.`);
this.config.interval = ms;
this.start();
} else {
this.config.interval = ms;
}
return this;
}
// Start the interval function that polls the endpoint
start = () => {
if (this.fetchInterval) {
clearInterval(this.fetchInterval);
console.log(`DataManager: Already running! Clearing interval so it can be restarted.`);
}
this.fetchInterval = setInterval(async () => {
if (!this.fetching) {
console.log(`DataManager: Fetching data for endpoint "${this.config.endpoint}".`);
this.fetching = true;
// const res = await axios.get(this.config.endpoint);
// Commented out for demo purposes but you would uncomment this and clear the anonymous function below
const res = {};
(() => {
res.data = {
dataProp1: 1234,
dataProp2: 4567
}
})();
this.fetching = false;
this.data = res.data;
} else {
console.log(`DataManager: Waiting for pending response for endpoint "${this.config.endpoint}".`);
}
}, this.config.interval);
return this;
}
// Stop the interval function that polls the endpoint
stop = () => {
if (this.fetchInterval) {
clearInterval(this.fetchInterval);
console.log(`DataManager: Endpoint "${this.config.endpoint}" stopped.`);
} else {
console.log(`DataManager: Nothing to stop for endpoint "${this.config.endpoint}".`);
}
return this;
}
}
const SharedComponentState = {
source1: new DataManager({
interval: 1000,
endpoint: `/data/one/someId`,
autostart: true
}),
source2: new DataManager({
interval: 5000,
endpoint: `/data/two/someId`,
autostart: true
}),
source3: new DataManager({
interval: 10000,
endpoint: `/data/three/someId`,
autostart: true
})
};
setTimeout(() => { // For Demo Purposes, Stopping and starting DataManager.
SharedComponentState.source1.stop();
SharedComponentState.source1.updateInterval(2000);
SharedComponentState.source1.start();
}, 10000);
// Heres what it would look like to access the DataManager data (fetched from the api)
// You will need to make sure you pass the SharedComponentState object as a prop to the components or use another React mechanism for making that SharedComponentState accessible to the components in your app
// Accessing state for source 1: SharedComponentState.source1.data
// Accessing state for source 2: SharedComponentState.source2.data
// Accessing state for source 3: SharedComponentState.source3.data
Basically, each instance of the DataManager class is responsible for fetching a different api endpoint. I included a few other class methods that allow you to start, stop and update the polling frequency of the DataManager instance.

Append a key/value pair to preexisting method that returns an object

I'm using the Heremaps API geocode method and I'm wondering if there is a way that I can append my own key/value pair that is an id of the record address that I'm running through the Heremaps API. The problem I'm running into is that the geocode method only takes an address parameter that it adds to the geocode endpoint call as its query string, but it doesn't accept any more arguments. I'm trying to come up with a way to call the geocode method with my address and append the record ID into the object that is returned so that each address that the API returns, has its original ID.
Since I can't change the geocode method because it's being called from a cdn https://js.api.here.com/v3/3.1/mapsjs-service.js, I need to append the record ID to the address that's being returned.
I added a coupleGeocodeRecordID function to attempt to couple the record ID with the object returned from geocode function call but it returns an error
Uncaught (in promise) TypeError: can't access property 1533, geoCoder(...) is undefined
app.js that makes the API call to the database to retrieve the record IDs (3), location names (6) and addresses (13) in the refSrcData const. The fetchAll call is just an API call to the database to pull these values.
const refSrcData = {
from: 'xxxxxxx',
select: [3, 6, 13],
};
const fetchAll = () => {
fetch('call to the endpoint', {
method: 'GET',
headers: {
'QB-Realm-Hostname': 'xxxxxxxxx',
Authorization: 'xxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify(refSrcData),
})
.then(resp => resp.json())
.then(data => {
getRefAddresses(data);
})
.catch(error => console.log('Error:', error));
};
// Step 1: initialize communication with the platform
const platform = new H.service.Platform({
apikey: 'xxxxxxxxxxxxxxxxxxxxx',
});
// Get an instance of the geocoding service:
const service = platform.getSearchService();
// Call the geocode method with the geocoding parameters,
// the callback and an error callback function (called if a
// communication error occurs):
const geoCoder = address => {
try {
service.geocode({
q: address,
},
result => {
// Add a marker for each location found
result.items.forEach(item => {
console.log(item);
});
}
);
} catch (err) {
console.log(err, `Can't reach the remote server`);
}
};
// Write a function that handles the record ID and couples with the geocode data
const coupleGeocodeRecordID = (address, recordId) => {
geoCoder(address)[recId] = recordId;
};
const getRefAddresses = async dataObject => {
const recordId = dataObject.data.map(e => e['3'].value);
const refAddress = dataObject.data.map(e => e['6'].value);
const refName = dataObject.data.map(e => e['13'].value);
for (let i = 0; i <= 10; i++) {
let location = refAddress[i];
location.length > 0
?
await coupleGeocodeRecordID(location, recordId[i]) :
false;
}
};
window.onload = () => {
fetchAll();
};
This is what is returned when I call the geocode function. I'm trying to attach the record ID to each object.
There are two problems:
Your geoCoder() function was not returning the result after the search. It was only logging it to the console. (You have to use promises with the geocode API, but promises work with async / await. I hope it makes sense)
Your coupleGeocodeRecordId() function is not doing anything with the results. I assume you plan to use them somehow. (Here, they are stored in an object called results for later use.)
I modified those two functions. The rest of your code looks fine, probably.
I can't test this code, so I don't know if there are other problems with how you're using the API.
const results = {};
const geoCoder = address => new Promise( finish => {
try {
//try to run the search.
service.geocode(
{ q: address, },
finish //on success, resolve the promise with the result
);
} catch ( err ) {
console.error( "Can't reach the remote server.", err );
finish( null ); //on failure, resolve the promise with null
}
} );
const coupleGeocodeRecordID = async ( address, recordId ) => {
//try to get a search result.
const result = await geoCoder( address );
if( result ) {
//attach the record id to the search result
result.recordId = recordId;
//store the result for use? I don't know what these are for
results[ address ] = result;
}
}

How to fix Cloud Function error admin.database.ref is not a function at exports

I'm currently trying to modify my Cloud Functions and move in under https.onRequest so that i can call use it to schedule a cron job. How it i'm getting the following error in the logs.
TypeError: admin.database.ref is not a function
at exports.scheduleSendNotificationMessageJob.functions.https.onRequest (/user_code/index.js:30:20)
at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:57:9)
exports.scheduleSendNotificationMessageJob = functions.https.onRequest((req, res) => {
admin.database.ref('/notifications/{studentId}/notifications/{notificationCode}')
.onCreate((dataSnapshot, context) => {
const dbPath = '/notifications/' + context.params.pHumanId + '/fcmCode';
const promise = admin.database().ref(dbPath).once('value').then(function(tokenSnapshot) {
const theToken = tokenSnapshot.val();
res.status(200).send(theToken);
const notificationCode = context.params.pNotificationCode;
const messageData = {notificationCode: notificationCode};
const theMessage = { data: messageData,
notification: { title: 'You have a new job reminder' }
};
const options = { contentAvailable: true,
collapseKey: notificationCode };
const notificationPath = '/notifications/' + context.params.pHumanId + '/notifications/' + notificationCode;
admin.database().ref(notificationPath).remove();
return admin.messaging().sendToDevice(theToken, theMessage, options);
});
return null;
});
});
You cannot use the definition of an onCreate() Realtime Database trigger within the definition of an HTTP Cloud Function.
If you switch to an HTTP Cloud Function "so that (you) can call use it to schedule a cron job" it means the trigger will be the call to the HTTP Cloud Function. In other words you will not be anymore able to trigger an action (or the Cloud Function) when new data is created in the Realtime Database.
What you can very well do is to read the data of the Realtime Database, as follows, for example (simplified scenario of sending a notification):
exports.scheduleSendNotificationMessageJob = functions.https.onRequest((req, res) => {
//get the desired values from the request
const studentId = req.body.studentId;
const notificationCode = req.body.notificationCode;
//Read data with the once() method
admin.database.ref('/notifications/' + studentId + '/notifications/' + notificationCode)
.once('value')
.then(snapshot => {
//Here just an example on how you would get the desired values
//for your notification
const theToken = snapshot.val();
const theMessage = ....
//......
// return the promise returned by the sendToDevice() asynchronous task
return admin.messaging().sendToDevice(theToken, theMessage, options)
})
.then(() => {
//And then send back the result (see video referred to below)
res.send("{ result : 'message sent'}") ;
})
.catch(err => {
//........
});
});
You may watch the following official Firebase video about HTTP Cloud Functions: https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3. It shows how to read data from Firestore but the concept of reading and sending back the response (or an error) is the same for the Realtime Database. Together with the 2 other videos of the series (https://firebase.google.com/docs/functions/video-series/?authuser=0), it also explains how it is important to correctly chain promises and to indicate to the platform that the work of the Cloud Function is finished.
For me, this error happened when writing admin.database instead of admin.database().

Redux async requests with fetch api

I'm stuck in a wierd behaviour that I can't really debug.
The store dispatch the action that perform the login request passing username and password. Then when the response is ready I store the credentials in the redux store. When I need to perform an authorized request I set those parameters in the header request. When I receive the response I update the credentials in the store with the new ones that I get from the response.
When I try to perform the third request it will respond unauthorized. I figured out that this is because all the parameters passed to my action generator setCredentials are null. I can't understand why also because if I add a debugger before the return statement of my setCredentials function and I wait some seconds before restart the execution I found out that the parameters aren't null anymore. I was thinking about the fact that the request is async but being inside a then statement the response should be ready right? I've also notice that fetch sent two request for each one.
Here the code for more clarity.
import { combineReducers } from 'redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const initialState = {
currentUser: {
credentials: {},
user: {}
},
test: {},
users: []
}
export const SUBMIT_LOGIN = 'SUBMIT_LOGIN'
export const SET_USER = 'SET_USER'
export const TEST = 'TEST'
export const SET_USERS = 'SET_USERS'
export const SET_CREDENTIALS = 'SET_CREDENTIALS'
//actions
const submitLogin = () => (dispatch) => {
return postLoginRequest()
.then(response => {
dispatch(setCredentials(
response.headers.get('access-token'),
response.headers.get('client'),
response.headers.get('expiry'),
response.headers.get('token-type'),
response.headers.get('uid')
));
return response
})
.then(response => {
return response.json();
})
.then(
(user) => dispatch(setUser(user.data)),
);
}
const performRequest = (api) => (dispatch) => {
return api()
.then(response => {
dispatch(setCredentials(
response.headers.get('access-token'),
response.headers.get('client'),
response.headers.get('expiry'),
response.headers.get('token-type'),
response.headers.get('uid')
));
return response
})
.then(response => {return response.json()})
.then(
(users) => {
dispatch(setUsers(users.data))
},
);
}
const setUsers = (users) => {
return {
type: SET_USERS,
users
}
}
const setUser = (user) => {
return {
type: SET_USER,
user
}
}
const setCredentials = (
access_token,
client,
expiry,
token_type,
uid
) => {
debugger
return {
type: SET_CREDENTIALS,
credentials: {
'access-token': access_token,
client,
expiry,
'token-type': token_type,
uid
}
}
}
//////////////
const currentUserInitialState = {
credentials: {},
user: {}
}
const currentUser = (state = currentUserInitialState, action) => {
switch (action.type) {
case SET_USER:
return Object.assign({}, state, {user: action.user})
case SET_CREDENTIALS:
return Object.assign({}, state, {credentials: action.credentials})
default:
return state
}
}
const rootReducer = combineReducers({
currentUser,
test
})
const getAuthorizedHeader = (store) => {
const credentials = store.getState().currentUser.credentials
const headers = new Headers(credentials)
return headers
}
//store creation
const createStoreWithMiddleware = applyMiddleware(
thunk
)(createStore);
const store = createStoreWithMiddleware(rootReducer);
const postLoginRequest = () => {
return fetch('http://localhost:3000/auth/sign_in', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'test#test.com',
password: 'password',
})
})
}
const getUsers = () => {
const autorizedHeader = getAuthorizedHeader(store)
return fetch('http://localhost:3000/users',
{
method: 'GET',
headers : autorizedHeader
}
)
}
const getWorks = () => {
const autorizedHeader = getAuthorizedHeader(store)
return fetch('http://localhost:3000/work_offers',
{
method: 'GET',
headers : autorizedHeader
}
)
}
// this request works fine
store.dispatch(submitLogin())
// this request works fine
setTimeout(() => {
store.dispatch(performRequest(getUsers))
}, 3000)
// this fails
setTimeout(() => {
store.dispatch(performRequest(getWorks))
}, 5000)
I should have clarified that when I asked
Have you verified that all your endpoints return those headers and not just the login one? Maybe when you performRequest(getUsers), it comes back with empty headers.
I didn’t just mean the server logic. I meant opening the Network tab in DevTools and actually verifying whether your responses contain the headers you expect. It turns out getUsers() headers do not always contain the credentials:
Now that we confirmed this happens, let’s see why.
You dispatch submitLogin() and performRequest(getUsers) roughly at the same time. In the cases when the error is reproduced, the problem is in the following sequence of steps:
You fire off submitLogin()
You fire off performRequest(getUsers) before submitLogin() comes back
submitLogin() comes back and stores the credentials from the response headers
performRequest(getUsers) comes back but since it started before credentials were available, the server responds with empty headers, and those empty credentials are stored instead of the existing ones
performRequest(getWorks) is now requested without the credentials
There are several fixes for this problem.
Don’t Let Old Unauthorized Requests Overwrite the Credentials
I don’t think it really makes sense to overwrite existing good credentials with the empty ones, does it? You can either check that they are non-empty in performRequest before dispatching:
const performRequest = (api) => (dispatch, getState) => {
return api()
.then(response => {
if (response.headers.get('access-token')) {
dispatch(setCredentials(
response.headers.get('access-token'),
response.headers.get('client'),
response.headers.get('expiry'),
response.headers.get('token-type'),
response.headers.get('uid')
));
}
return response
})
.then(response => {return response.json()})
.then(
(users) => {
dispatch(setUsers(users.data))
},
);
}
Alternatively, you can do ignore invalid credentials in the reducer itself:
case SET_CREDENTIALS:
if (action.credentials['access-token']) {
return Object.assign({}, state, {credentials: action.credentials})
} else {
return state
}
Both ways are fine and depend on the conventions that make more sense to you.
Wait Before Performing Requests
In any case, do you really want to fire getUsers() before you have the credentials? If not, fire off the requests only until the credentials are available. Something like this:
store.dispatch(submitLogin()).then(() => {
store.dispatch(performRequest(getUsers))
store.dispatch(performRequest(getWorks))
})
If it’s not always feasible or you would like more sophisticated logic like retrying failed requests, I suggest you to look at Redux Saga which lets you use powerful concurrency primitives to schedule this kind of work.

Categories

Resources