Make an ajax call inside of a .map() - ajax

I am upgrading jquery and saw that the "async: false" option has been deprecated. This makes sense and in 99.9% of cases I agree with the rationale, but I have a case where I think I really need it and I cannot for the life of me figure out how to make this work with a purely async ajax call no matter how I use promises or async/await.
My use case is in a Vue component and I have an array of contacts. What I need to do is map over the contacts and validate them. One such validation requires a quick check of email validity via a "check_email" ajax endpoint.
Once I validate (or not) the list, I then submit the list (if valid) or show error messages (if invalid).
My code is something like this
sendContacts: function() {
valid = this.validateContacts()
if (valid) {
// send the contacts
} else {
return // will display error messages on contacts objects
}
},
validateContacts: function() {
this.contacts = this.contacts.map((contact) => {
if (!contact.name) {
contact.validDetails.name = false
contact.valid = false
return contact
}
if (!contact.email) {
contact.validDetails.emailExists = false
contact.valid = false
return contact
}
if (!check_email(email)) { // THIS IS ASYNC NOW WHAT DO I DO
contact.valid = false
contact.validDetails.emailFormat = false
}
return contact
}
var validData = this.contacts.map(c => {
return c.valid
})
return !validData.includes(false)
}
function check_email(email) {
const url = `/api/v1/users/check-email?email=${email}`
let valid = false
$.ajax({
url: url,
type: 'POST',
async: false, // I can't do this anymore
headers: {
'X-CSRFToken': csrfToken
},
success: resp => {
valid = true
},
error: err => {
}
})
return valid
}
my data function:
data: function() {
return {
contacts: [this.initContact()],
showThanks: false,
emailError: false,
blankEmail: false,
blankName: false
}
},
methods: {
initContact: function() {
return {
name: null,
email: null,
title: null,
validDetails: this.initValidDetails(),
valid: true,
}
},
initValidDetails: function() {
return {
emailDomain: true,
emailExists: true,
emailFormat: true,
name: true
}
}
}
Again, I have tried async/await in every place I could think of and I cannot get this to validate properly and then perform correct logic regarding whether the send contacts function part of the function should fire. Please help!

Once any part of your validation is asynchronous, you must treat the entire thing as asynchronous. This includes when calling validateContacts in sendContacts.
First, you should change check_email to return Promise<bool>. It's usually a bad idea to include jQuery in a Vue project so let's use fetch instead (Axios being another popular alternative).
async function check_email(email) {
const params = new URLSearchParams({ email })
const res = await fetch(`/api/v1/users/check-email?${params}`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
return res.ok
}
As for your async validation logic, it's best to map your contacts to an array of promises and wait for them all with Promise.all.
async validateContacts () {
const validationPromises = this.contacts.map(async contact => {
if (!contact.name) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
name: false
}
}
}
if (!contact.email) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailExists: false
}
}
}
if (await check_email(contact.email)) { // await here
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailFormat: false
}
}
}
return { ...contact, valid: true }
})
// now wait for all promises to resolve and check for any "false" values
this.contacts = await Promise.all(validationPromises)
return this.contacts.every(({ valid }) => valid)
}
As mentioned, now you need to treat this asynchronously in sendContacts
async sendContacts () {
if (await this.validateContacts()) {
// send the contacts
}
}

Related

Vue 3 components not awaiting for state to be loaded

I am having some trouble using fetch in vuex to build state before rendering my page's components.
Here is the page component code:
async beforeCreate() {
await this.$store.dispatch('projects/getProjects');
},
And this is the state code it's dispatching:
async getProjects(context: any, parms: any) {
context.commit("loadingStatus", true, { root: true });
console.log("1");
await fetch(`${process.env.VUE_APP_API}/projects?`, {
method: "get",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then((response) => {
console.log("2");
if (!response.ok) {
throw new Error(response.status.toString());
} else {
return response.json();
}
})
.catch((error) => {
// todo: tratamento de erros na UI
console.error("There was an error!", error);
})
.then((data) => {
context.commit("setProjects", { data });
console.log("3");
// sets the active project based on local storage
if (
localStorage.getItem(
`activeProjectId_${context.rootState.auth.operator.accountId}`
)
) {
console.log("setting project to storage");
context.dispatch("selectProject", {
projectId: localStorage.getItem(
`activeProjectId_${context.rootState.auth.operator.accountId}`
),
});
} else {
//or based on the first item in the list
console.log("setting project to default");
if (data.length > 0) {
context.dispatch("selectProject", {
projectId: data[0].id,
});
}
}
context.commit("loadingStatus", false, { root: true });
});
},
async selectProject(context: any, parms: any) {
console.log("4");
context.commit("loadingStatus", true, { root: true });
const pjt = context.state.projects.filter(
(project: any) => project.id === parms.projectId
);
if (pjt.length > 0) {
console.log("Project found");
await context.commit("setActiveProject", pjt[0]);
} else if (context.state.projects.length > 0) {
console.log("Project not found setting first on the list");
await context.commit("setActiveProject", context.state.projects[0]);
} else {
await context.commit("resetActiveProject");
}
await context.commit("loadingStatus", false, { root: true });
},
I've added this console.log (1, 2, 3, 4) to help me debug what's going on.
Right after console.logging "1", it starts to mount the components. And I only get logs 2, 3 and 4 after all components have been loaded.
How can I make it so that my components will only load after the whole process is done (i.e. after I log "4") ?
If your beforeCreate hook (or any client hooks) contains async code, Vue will NOT wait to it then render and mount the component.
The right choice here should be showing a loader when your data is fetching from the server. It will provide better UX:
<template>
<div v-if="!data"> Loading... </div>
<div v-else> Put all your logic with data here </div>
</template>
<script>
export default {
data() {
return {
data: null
}
},
async beforeCreate() {
this.data = await this.$store.dispatch('projects/getProjects');
},
}
</script>

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

Nest.js handling errors for HttpService

I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).

ASK error, TypeError: Cannot read property 'type' of undefined

I'm creating a skill that will call back different incidents at different dates and times from a DynamoDB table through Alexa.
My 3 columns are data, time and incident
I've defined my partition and sort key in my Lambda function as
let GetMachineStateIntent = (context, callback) => {
var params = {
TableName: "updatedincident",
Key: {
date: "2017-03-21",
time: "07:38",
incident: "Blocked Primary",
}
};
When I try to test my skill I can't seem to recall the incident correctly, the error I'm getting in Cloudwatch is:
2018-03-28T14:48:53.397Z 042319cb-4a3e-49ae-8b33-1641367107d4 Unexpected error occurred in the skill handler! TypeError: Cannot read property 'type' of undefined
at exports.handler.e (/var/task/index.js:70:16)
as well as:
2018-03-28T14:48:53.417Z 042319cb-4a3e-49ae-8b33-1641367107d4
{
"errorMessage": "Unexpected error"
}
Here is my code from index.js
var AWSregion = 'us-east-1'; // us-east-1
var AWS = require('aws-sdk');
var dbClient = new AWS.DynamoDB.DocumentClient();
AWS.config.update({
region: "'us-east-1'"
});
let GetMachineStateIntent = (context, callback) => {
var params = {
TableName: "updatedincident",
Key: {
date: "2018-03-28",
time: "04:23",
}
};
dbClient.get(params, function (err, data) {
if (err) {
// failed to read from table for some reason..
console.log('failed to load data item:\n' + JSON.stringify(err, null, 2));
// let skill tell the user that it couldn't find the data
sendResponse(context, callback, {
output: "the data could not be loaded from your database",
endSession: false
});
} else {
console.log('loaded data item:\n' + JSON.stringify(data.Item, null, 2));
// assuming the item has an attribute called "incident"..
sendResponse(context, callback, {
output: data.Item.incident,
endSession: false
});
}
});
};
function sendResponse(context, callback, responseOptions) {
if(typeof callback === 'undefined') {
context.succeed(buildResponse(responseOptions));
} else {
callback(null, buildResponse(responseOptions));
}
}
function buildResponse(options) {
var alexaResponse = {
version: "1.0",
response: {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.output}</prosody></speak>`
},
shouldEndSession: options.endSession
}
};
if (options.repromptText) {
alexaResponse.response.reprompt = {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.reprompt}</prosody></speak>`
}
};
}
return alexaResponse;
}
exports.handler = (event, context, callback) => {
try {
var request = event.request;
if (request.type === "LaunchRequest") {
sendResponse(context, callback, {
output: "welcome to my skill. what data are you looking for?",
endSession: false
});
}
else if (request.type === "IntentRequest") {
let options = {};
if (request.intent.name === "GetMachineStateIntent") {
GetMachineStateIntent(context, callback);
} else if (request.intent.name === "AMAZON.StopIntent" || request.intent.name === "AMAZON.CancelIntent") {
sendResponse(context, callback, {
output: "ok. good bye!",
endSession: true
});
}
else if (request.intent.name === "AMAZON.HelpIntent") {
sendResponse(context, callback, {
output: "you can ask me about incidents that have happened",
reprompt: "what can I help you with?",
endSession: false
});
}
else {
sendResponse(context, callback, {
output: "I don't know that one! please try again!",
endSession: false
});
}
}
else if (request.type === "SessionEndedRequest") {
sendResponse(context, callback, ""); // no response needed
}
else {
// an unexpected request type received.. just say I don't know..
sendResponse(context, callback, {
output: "I don't know that one! please try again!",
endSession: false
});
}
} catch (e) {
// handle the error by logging it and sending back an failure
console.log('Unexpected error occurred in the skill handler!', e);
if(typeof callback === 'undefined') {
context.fail("Unexpected error");
} else {
callback("Unexpected error");
}
}
};
and this is my handler GetMachineState.js
function sendResponse(context, callback, responseOptions) {
if(typeof callback === 'undefined') {
context.succeed(buildResponse(responseOptions));
} else {
callback(null, buildResponse(responseOptions));
}
}
function buildResponse(options) {
var alexaResponse = {
version: "1.0",
response: {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.output}</prosody></speak>`
},
shouldEndSession: options.endSession
}
};
if (options.repromptText) {
alexaResponse.response.reprompt = {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.reprompt}</prosody></speak>`
}
};
}
return alexaResponse;
}
exports.handler = (event, context, callback) => {
try {
var request = event.request;
if (request.type === "LaunchRequest") {
sendResponse(context, callback, {
output: "welcome to my skill. what do you want to find?",
endSession: false
});
}
else if (request.type === "IntentRequest") {
let options = {};
if (request.intent.name === "GetMachineStateIntent") {
// this is where we will wire up the dynamo call
// for now, just send a simple response and end the session
sendResponse(context, callback, {
output: "cinema not implemented yet!",
endSession: true
});
} else if (request.intent.name === "AMAZON.StopIntent" || request.intent.name === "AMAZON.CancelIntent") {
sendResponse(context, callback, {
output: "ok. good bye!",
endSession: true
});
}
else if (request.intent.name === "AMAZON.HelpIntent") {
sendResponse(context, callback, {
output: "you can ask me about incidents that have happened",
reprompt: "what can I help you with?",
endSession: false
});
}
else {
sendResponse(context, callback, {
output: "I don't know that one! Good bye!",
endSession: true
});
}
}
else if (request.type === "SessionEndedRequest") {
sendResponse(context, callback, ""); // no response needed
}
else {
// an unexpected request type received.. just say I don't know..
sendResponse(context, callback, {
output: "I don't know that one! Good bye!",
endSession: true
});
}
} catch (e) {
// handle the error by logging it and sending back an failure
console.log('Unexpected error occurred in the skill handler!', e);
if(typeof callback === 'undefined') {
context.fail("Unexpected error");
} else {
callback("Unexpected error");
}
}
};
Its impossible to know for sure if this is the problem or not because you haven't shared the code from the index.js file. The error message you get is telling you that the problem occurs at line 70 in your index.js file so you should look there, and figure out what the problem is.
However, based on the fact that you also posted this as a comment on another question, I'm going to venture to guess that the issue you've run into is that you used the code snippet I provided in the answer to that question and the error is from dereferencing the request.type
You have to make sure the request variable is set to the actual request from the event, like so: var request = event.request where event is provided from exports.handler = (event, context, callback) => {
For example:
exports.handler = (event, context, callback) => {
var request = event.request;
if (request.type === "IntentRequest"
// make suret the name of the intent matches the one in your interaction model
&& request.intent.name == "GetMachineStateIntent") {
var dateSlot = request.intent.slots.Date != null ?
request.intent.slots.Date.value : "unknown date";
var timeSlot = request.intent.slots.Time != null ?
request.intent.slots.Time.value : "unknown time";
// respond with speech saying back what the skill thinks the user requested
sendResponse(context, callback, {
output: "You wanted the machine state at "
+ timeSlot + " on " + dateSlot,
endSession: true
});
} else {
// TODO: handle other types of requests..
sendResponse(context, callback, {
output: "I don't know how to handle this request yet!"
endSession: true
});
}
};
function sendResponse(context, callback, responseOptions) {
if(typeof callback === 'undefined') {
context.succeed(buildResponse(responseOptions));
} else {
callback(null, buildResponse(responseOptions));
}
}
function buildResponse(options) {
var alexaResponse = {
version: "1.0",
response: {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.output}</prosody></speak>`
},
shouldEndSession: options.endSession
}
};
if (options.repromptText) {
alexaResponse.response.reprompt = {
outputSpeech: {
"type": "SSML",
"ssml": `<speak><prosody rate="slow">${options.reprompt}</prosody></speak>`
}
};
}
return alexaResponse;
}

Proper way to clear asynchronous work in redux middleware

I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
if an error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware
class Autocomplete extends Component{
componentWillMount() {
setTimeout(() => {
this.props.dispatch(actions.init({ props: this.exportProps() }));
});
}
Should I have the callApi function execute on a separate event loop tick?

Resources