Offline JavaScript Broker

    In my project, I needed a functional that would allow not to lose the entered data, in case of internet connection interruption, and I came up with a very simple “Broker”, which allowed not to lose data when the connection was lost, and send them when the connection was restored again. Perhaps “Broker” is not a very good name for him, but do not judge strictly. I want to share, maybe someone will be useful.

    about the project


    My project is designed to account for expenses and income, or as a simple version of home accounting. It was created as a progressive web application, so that it would be convenient to use it on mobile devices, as well as to open up the possibilities of push notifications, access the camera for reading bar codes, and the like. There is a similar mobile application, called ZenMoney, but I wanted something of my own and in my own way.

    The emergence of ideas


    I try to keep track of expenses and incomes clearly, but since it is often forgotten to bring in the necessary positions, especially with regard to cash, I have to do it almost immediately as a “transaction” occurred. Sometimes I entered data in public transport, such as the subway, where connection loss often occurs even in spite of the widespread Wi-Fi network. It was a shame that everything hangs up and nothing happens, and then the data is simply lost.

    The idea came through the use of a queue broker, such as RabbitMQ. Of course, I have a simpler and less functional solution, but there is something similar to its principles. I thought that you can save everything, for example, in Cache or LocalStorage in the form of an object with a queue of "unsatisfied" requests, and when a connection appears, you can easily execute the connection. Of course, they are not executed in a queue order, which is even better due to asynchronous processing of requests in the JS language, given that you have only one “subscriber”. I faced some difficulties, maybe even the implementation of this all will seem a little curve, but this is a working solution. Of course, it can be improved, but for the time being I will describe the existing “raw” but working version.

    Getting Started


    The first thing I thought about was where to store data in the absence of a connection? The service terminal imposed on me PWA, works well with the cache, but is it wise to use the cache ?! Difficult question, I will not go into it. In general, I decided that LocalStorage is better suited to me. Since LocalStorage stores values ​​of type key: value, the object had to be added as a Json string. In my project for external development, I added, in the folder with classes, a directory called QueueBroker

    File structure
    /**----**/
    ├── app.js
    ├── bootstrap.js
    ├── classes
    │   └── QueueBroker
    │   ├── index.js
    │   └── Library
    │   ├── Broker.js
    │   └── Storage.js
    ├── components
    /**----**/


    My project is made in the Laravel + VueJs stack, so a certain dependency of the file structure is required. I do not know how in such cases it is correct to call your own directories for classes, therefore I did so.

    The index file is created to simply connect the modules from the nested Library. It may not be a very elegant solution, but I wanted to make it so that if I suddenly changed my mind about using LocalStorage, I would write another class for Storage with the same methods, pass it to the broker's designer, and without changing anything, I would use another storage.

    index.js
    const Broker = require('./Library/Broker');
    const Storage = require('./Library/Storage');
    module.exports.Broker = Broker;
    module.exports.Storage = Storage;
    


    This method allows you to connect only those libraries that I need in my scripts, for example, if I need both:

    import {Storage, Broker} from'../../classes/QueueBroker/index';
    

    To make it easy for me to change the storage class, I made a semblance of the constructor for the Broker class, in which the Storage object could be passed as an argument, as long as it has the necessary functions. I know that on ES6 I could write class and constructor, but decided to do it the old way - prototype. Comments will write directly on the code:

    Broker.js
    const axios = require('axios'); //Мне нравится axios/*
    Это и есть подобие конструктора.
    Префикс нужен для того, чтобы мы могли использовать разные объекты для хранения в разных частях front-end приложения
    */functionBroker(storage, prefix='storageKey') {
        this.storage = storage;
        this.prefix = prefix;
        /*
        Если наш брокер пока пустой, мы загоним в него пустой объект. Главное чтобы storage с функцией add умел преобразовать его в json
        */if(this.storage.get('broker') === null) {
            this.broker = {};
            this.storage.add('broker', this.broker)
        }
        else {
            //А здесь наоборот, Storage должен уметь из Json отдать нам объект который мы запишем в свойство нашего прототипного классаthis.broker = this.storage.getObject('broker');
        }
    };
    //Просто счетчик, чтобы мы могли определить сколько сообщений ожидает отправки на сервер
    Broker.prototype.queueCount = function () {
        returnObject.keys(this.broker).length;
    };
    //Метод сохранения "неудовлетворенного" запроса в наш Storage, с присвоением ключа
    Broker.prototype.saveToStorage = function (method, url, data) {
        let key = this.prefix + '_' + (Object.keys(this.broker).length + 1);
        this.broker[key] = {method, url, data};
        //Кстати здесь тоже желательно сделать разные ключи а не записывать все в broker, но для упрощения примера решил оставить такthis.storage.add('broker', this.broker);
    };
    //Это метод, который будет отправлять данные, когда восстановится соединение
    Broker.prototype.run = function () {
        for (let key inthis.broker) {
            this.sendToServer(this.broker[key], key)
        }
    }
    /*
    Метод отправки на сервер. Нам нужен объект с записанными данными для отправки, который содержит в себе method, url и data, а так же ключ элемента в нашем хранилище, чтобы удалить по нему, после успешной отправки
    */
    Broker.prototype.sendToServer = function (object, brokerKey) {
        axios({
            method: object.method,
            url: object.url,
            data: object.data,
        })
        .then(response => {
            if(response.data.status == 200) {
                //Удаляем объект по ключу, после успешной отправкиdeletethis.broker[brokerKey];
                //Перезаписываем объектthis.storage.add('broker', this.broker);
            }
            else {
                //оставим для дебага ;-)console.log(response.data)
            }
        })
        .catch(error => {
             /*
             Ну и кончено после всех успешных испытаний сделаем красивый отлов ошибок, но не в рамках данной статьи
            */
        });
    };
    //Не забываем сделать exportmodule.exports = Broker;
    


    Next, you need the Storage object itself, which will successfully save and retrieve everything from the storage.

    Storage.js
    //Возможность включить debug-режим для отладкиfunctionStorage(debug) {
        if(debug === true)
        {
            this.debugMode = true;
        }
        this.storage = window.localStorage;
    };
    //Специальный метод, для преобразования объекта в Json и сохранении его в хранилище
    Storage.prototype.addObjectToStorage = function (key, object) {
        this.storage.setItem(key, JSON.stringify(object));
    };
    //Для записи остальных параметров (чисел, булевых и строк)
    Storage.prototype.addStringToStorage = function (key, value) {
        this.storage.setItem(key, value);
    };
    //Получение элемента из хранилища
    Storage.prototype.get = function (key) {
        returnthis.storage.getItem(key);
    };
    //Получение объекта из нашего Json внутри, который мы записали другим методом выше
    Storage.prototype.getObject = function (key) {
        try
        {
            returnJSON.parse(this.storage.getItem(key));
        }
        catch (e)
        {
            this._debug(e);
            this._debug(key + ' = ' + this.storage.getItem(key));
            returnfalse;
        }
    };
    /*
    Добавление, чтобы не заморачиваться с методами, отдаем ему, а он уже сам выбирает как его записать, сериализовать в Json или записать в чистом виде
    */
    Storage.prototype.add = function (key, value) {
        try
        {
            if(typeof value === 'object') {
                this.addObjectToStorage(key, value);
            }
            elseif (typeof value === 'string' || typeof value === 'number') {
                this.addStringToStorage(key, value);
            }
            else {
                //Небольшая проверка на типыthis._debug('2 parameter does not belong to a known type')
            }
            returnthis.storage;
        }
        catch (e)
        {
           //Защита от переполнения хранилища встроенная, но нам нужно знать, если такое случитсяif (e === QUOTA_EXCEEDED_ERR) {
                this._debug('LocalStorage is exceeded the free space limit')
            }
            else
            {
                this._debug(e)
            }
        }
    };
    //Очистка хранилища
    Storage.prototype.clear = function () {
        try
        {
            this.storage.clear();
            returntrue;
        }
        catch (e)
        {
            this._debug(e)
            returnfalse;
        }
    };
    //Удаление элемента из хранилища
    Storage.prototype.delete = function(key) {
        try
        {
            this.storage.removeItem(key);
            returntrue;
        }
        catch (e)
        {
            this._debug(e)
            returnfalse;
        }
    };
    //Маленький дебагер, которрый мы используем по ходу
    Storage.prototype._debug = function(error) {
        if(this.debugMode)
        {
            console.error(error);
        }
        returnnull;
    };
    //Не забываем экспортироватьmodule.exports = Storage;
    


    When all the above will be done, it can be used at your discretion, I use it like this:

    Use when saving
    //это внутри объекта Vue (methods)/*----*///Здесь и объявляем наш брокер и Storage для него
    sendBroker(method, url, data) {
                let storage = new Storage(true);
                let broker = new Broker(storage, 'fundsControl');
                broker.saveToStorage(method, url, data);
            },
    //Здесь выполняем свой обычный запрос
    fundsSave() {
                let url = '/pa/funds';
                let method = '';
                if(this.fundsFormType === 'create') {
                    method = 'post';
                }
                elseif(this.fundsFormType === 'update') {
                    method = 'put';
                }
                elseif(this.fundsFormType === 'delete') {
                    method = 'delete';
                }
                this.$store.commit('setPreloader', true);
                axios({
                    method: method,
                    url: url,
                    data: this.fundsFormData,
                })
                    .then(response=> {
                        if(response.data.status == 200) {
                            this.fundsFormShow = false;
                            this.getFunds();
                            this.$store.commit('setPreloader', false);
                        }
                        else {
                            this.$store.commit('AlertError', 'Ошибка получения данных с сервера');
                        }
                    })
                    //А как раз здесь отлавливаем нашу ошибку соединения
                    .catch(error => {
                        this.$store.commit('setAlert',
                            {
                                type: 'warning',
                                status: true,
                                message: 'Ошибка соединения с сервером. Однако, ваши данные не будут уреряны и будут записаны, после восстановления соединения'
                            }
                            );
                        this.fundsFormShow = false;
                        this.$store.commit('setPreloader', false);
                       //И записываем наш "неудовлетворенный" запросthis.sendBroker(method, url, this.fundsFormData);
                        console.error(error);
                    });
            },
    


    Use when reconnecting
    //Это код компонента Vue/*--*/
    methods: {
    /*----*//*
    Инициация нашего брокера, с теми же параметрами, чтобы мы знали, что работаем с теми же ключами, с которыми записывали в брокер
    */
    brokerSendRun()
            {
                let storage = new Storage(true);
                let broker = new Broker(storage, 'fundsControl');
                //Проверяем, что есть что-то не отправленноеif(broker.queueCount() > 0)
                {
                    //Запускаем метод, который все отправит
                    broker.run();
                    //Выводим общий алерт приложения, с уведомлениемthis.$store.commit('setAlert', {type: 'info', status: true, message: 'Есть сообщения не отправленные на сервер из-за ошибок соединения, проверьте, что все данные успешно сохранены сейчас'});
                }
            }
    }
    /*---*//*
    Ну и вызываем наш метод, например, при монтировании компонента, как раз скорее всего после оторвавшегося соединения будет перезагрузка страницы, и уж если она загрузится, то и наши сообщения отправятся на сервер
    */
    mounted() {
            this.brokerSendRun();
    }
    /*---*/


    PS


    I find it difficult to talk about the code, so I tried to provide the code given in the examples with detailed comments as detailed as possible. If you have ideas for improving this solution or for improving this article, I will be glad to see them in the comments. I took examples from my own project on Vue, explaining this in order to make it clear why my methods are so called and why I refer to them through this . I do not do this article on Vue, so I don’t provide other component code, I leave it just for understanding.

    Also popular now: