⬆️ ⬇️

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 for cost accounting and income, or as a simple version of home bookkeeping. 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 as soon as the β€œ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 from 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, calmly execute them. Of course, they are not executed in the order of turn, 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 to the development



The first thing I thought about was where to store data in the absence of a connection? The service-server 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  */ function Broker(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 () { return Object.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 in this.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) { //   ,    delete this.broker[brokerKey]; //  this.storage.add('broker', this.broker); } else { //   ;-) console.log(response.data) } }) .catch(error => { /*           ,       */ }); }; //   export module.exports = Broker; 




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



Storage.js
 //  debug-   function Storage(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) { return this.storage.getItem(key); }; //    Json ,       Storage.prototype.getObject = function (key) { try { return JSON.parse(this.storage.getItem(key)); } catch (e) { this._debug(e); this._debug(key + ' = ' + this.storage.getItem(key)); return false; } }; /* ,     ,  ,        ,   Json      */ Storage.prototype.add = function (key, value) { try { if(typeof value === 'object') { this.addObjectToStorage(key, value); } else if (typeof value === 'string' || typeof value === 'number') { this.addStringToStorage(key, value); } else { //    this._debug('2 parameter does not belong to a known type') } return this.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(); return true; } catch (e) { this._debug(e) return false; } }; //    Storage.prototype.delete = function(key) { try { this.storage.removeItem(key); return true; } catch (e) { this._debug(e) return false; } }; // ,      Storage.prototype._debug = function(error) { if(this.debugMode) { console.error(error); } return null; }; //   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'; } else if(this.fundsFormType === 'update') { method = 'put'; } else if(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 much 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 give other component code, I leave it just for understanding.

Source: https://habr.com/ru/post/428980/



All Articles