Отправка данных на сервер и Promise (help) [как дождаться всех ответов от нескольких промисов?]

Привет всем. Столкнулся с проблемой, если кто-то может, помогите )

Есть коллекция объектов. У каждой коллекции есть ключ modified (true/false), чтобы определить редактировалась ли коллекция или нет, при отправке на сервер.

Допустим я беру и отрисовываю на странице одну из коллекции в виде таблицы, изменю там данные и так далее, проставляю самой коллекции modified (true) и вложенным объектам(строкам таблицы), которые были изменены, тоже проставляю modified (true).

Дальше запускаю метод в котором выполняется отправка изменени на сервер с помощью axios put и вот тут у меня возникает проблема.

Когда я пробегаюсь циклом по объекту, нахожу объекты у которых modified (true), я их отправляю и на них у меня стоит Promise, тут все норм. После успешной отправки я ставлю им статус false

Но, как мне сделать так, чтоб я мог дождаться не только отправки одного объекта, а дождаться когда все операции замены будут проведены и после этого, если все пройдет успешно, проставить уже статус modified (false) у самой коллекции???

function Storeage() {
    this.objects = [];
}

/*метода синхронизиурет данные из коллекции хранилища на сервер*/
Storeage.prototype.saveToDBbyRest = function() {

    const collections = this.objects;
    
    for ( var key in collections ) {
        if ( collections[key].modified === true ) {
    
            let nameCollection = collections[key].nameCollection;
            let obj_collection = collections[key].dataObj;
    
            for (var i = 0; i < obj_collection.length; i++) {
                let obj_collection_item  = obj_collection[i];
                if ( obj_collection_item.modified === true ) {
                    this.putCollection(nameCollection, obj_collection_item, collections[key]);
                }
            }
    
            // this.modifiedStatus(collections[key], false); Этот метод нужно вызвать после того, как все изменения уйдут на сервер.
        }
    }

}

Storeage.prototype.putCollection = function(name, item_collection, collection) {
    let id_obj = item_collection.id;
    let item_obj = item_collection.dataObj;
    /*axios put*/
    apiServices.putResult(name+'/'+id_obj+'.json', item_obj)
    .then(result => {
        if (result.data === 1) { console.log('Строка с ID - ' +  id_obj + ' была изменена') }
        if (result.data === 0) { console.log('Строка с ID - ' +  id_obj + ' не может быть отредактирована, возможно ее уже не существует') }
        this.modifiedStatus(item_collection, false);
    })
    .catch(error => {
        console.log('Ошибка в методе putResult, при отправки на сервер, что-то пошло не так')
    })
}

Ну и вот как выглядит сама коллекция.

Для этого есть стандартный метод Promise.all, который принимает массив собственно промисов. А чтоб удобнее было сформировать этот массив, пользоваться стандартными методами Array.filter и Array.map

function Storeage() {
    this.objects = [];
}

/*метода синхронизиурет данные из коллекции хранилища на сервер*/
Storeage.prototype.saveToDBbyRest = function() {

     const _self = this;
    const collections = this.objects;
    
    for ( var key in collections ) {
        if ( collections[key].modified === true ) {
    
            let nameCollection = collections[key].nameCollection;
            let obj_collection = collections[key].dataObj;

          Promise.all(
           obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.modified === true )
               .map((obj_collection_item) => {
                   return _self.putCollection(nameCollection, obj_collection_item, collections[key]);
               })
          ).then(() => {
                _self.modifiedStatus(collections[key], false); Этот метод нужно вызвать после того, как все изменения уйдут на сервер.
          });
        }
    }

}

Storeage.prototype.putCollection = function(name, item_collection, collection) {
    let id_obj = item_collection.id;
    let item_obj = item_collection.dataObj;
    /*axios put*/
    return apiServices.putResult(name+'/'+id_obj+'.json', item_obj)
    .then(result => {
        if (result.data === 1) { console.log('Строка с ID - ' +  id_obj + ' была изменена') }
        if (result.data === 0) { console.log('Строка с ID - ' +  id_obj + ' не может быть отредактирована, возможно ее уже не существует') }
        this.modifiedStatus(item_collection, false);
    })
    .catch(error => {
        console.log('Ошибка в методе putResult, при отправки на сервер, что-то пошло не так')
    })
}
2 лайка

Спасибо. Попробовал и все реально работает, но возникает вопрос.

Почему выходит так, что если все 3 запроса делать намерено с ошибкой, то получается вот что:

image

То есть, получается, что когда ему попался первый промис, который вернул ошибку, он вывел ошибку из promis.all, но после этого он продолжил выполнять отправку остальных объектов.

Почему?

            Promise.all(
                obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.modified === true )
                .map((obj_collection_item) => {
                    return _self.putCollection(nameCollection, obj_collection_item, collections[key]);
                })
                ).then(() => {
                    _self.modifiedStatus(collections[key], false);
                })
                .catch(() => {
                    console.log("Один из объектов в массиве не удалось отправить на сервер");
                })


Storeage.prototype.putCollection = function(name, item_collection, collection) {
    let id_obj = item_collection.id;
    let item_obj = item_collection.dataObj;
    /*axios put*/
    return apiServices.putResult(name+'2/'+id_obj+'.json', item_obj)
        .then(result => {
            if (result.data === 1) { console.log('Строка с ID - ' +  id_obj + ' была изменена') }
            if (result.data === 0) { console.log('Строка с ID - ' +  id_obj + ' не может быть отредактирована, возможно ее уже не существует') }
            this.modifiedStatus(item_collection, false);
            return resolve();
        })
        .catch(error => {
            console.log('Ошибка в методе putResult, при отправки на сервер, что-то пошло не так')
            return reject();
        })
}

Используйте в Promise эту технологиню XMLHttpRequest/abort - можно отменить в случае чего. Сам Promise отменить нельзя, он либо rejected либо fulfilled.

Все промисы будут пытаться быть отправлеными параллельно. Но если один из них свалился с ошибкой, то Promise.all вернет ошибку. Это поведение по умолчанию. Если представить себе другое поведение (например аборт запросов если один из запросов упал), то возникает куча вопросов - например а как быть если один из запросов успел завершится успешно до того как один из запросов завершился с ошибкой.

Если нужна логика аборта всех запросов при падении одного из них, то нужно искать библиотеку которая реализует желаемое поведение. Ну или писать самому.

Верно для стандартных промисов запросов. Библиотечные промисы запросов типа axios можно отменить: GitHub - axios/axios: Promise based HTTP client for the browser and node.js

1 лайк

стандартный fetch тоже можно отменить. вот пример

Собственно тогда можно создать один общий контроллер, у всех запросов прописать один и тот же signal и функцию для catch. И уже в ней вызывать abort. А так как сигнал прописан один для всех один и тот же, то и отменятся сразу все запросы при первой же ошибке.

1 лайк

Всем спасибо за ответы. Отчасти разобрался ).

Один вопросик еще, нормальная ли запись будет, если я буду формировать массив методами filter/ map дважды (так как разные условия). Или есть более короткая запись?
А то смотрю и кажется, что через “for” аккуратнее будет

obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.modified === true )
.map((obj_collection_item) => {
    return this.putCollection(nameCollection, obj_collection_item);
}),

obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.delete === true )
.map((obj_collection_item) => {
    return this.deleteCollection(nameCollection, obj_collection_item);
})

Не могу понять, помогите разобраться, как вот это можно реализовать через метод reduce ?
С учетом того что эти условия могут быть перекрестными (т.е. наример obj_collection_item может быть одновременно и modified == true и created == true )

Promise.all(
    
    obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.modified === true )
    .map((obj_collection_item) => {
        console.log(obj_collection_item);
        return this.putCollection(nameCollection, obj_collection_item);
    }).concat(
    
    obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.deleted === true )
    .map((obj_collection_item) => {
        console.log(obj_collection_item);
        return this.deleteCollection(nameCollection, obj_collection_item);
    })).concat(
    
    obj_collection.filter ( ( obj_collection_item ) => obj_collection_item.created === true )
    .map((obj_collection_item) => {
        console.log(obj_collection_item);
        return this.postCollection(nameCollection, obj_collection_item);
    }))

Просто такая реализация, как сейчас, довольно медленная

Массив обходится 6 раз. 3 раза filter, 3 раза map. Да ещё и две конкатенации в придачу. Конечно будет медленно.
При такой постановке задачи лучше сделать всё в одном map и там проверять условие и в зависимости значения возвращать то или иное значение

Promise.all(
    obj_collection.map( ( obj ) => {
        return obj.created ? this.postCollection(nameCollection, obj) :
            obj.deleted ? this.deleteCollection(nameCollection, obj) :
            obj.modified ? this.putCollection(nameCollection, obj) :
            Promise.resolve(); //  значение по дефолту.
    })
);