渐进式web应用开发-- 使用后台同步保证离线功能(六)
2019-07-29 19:52 龙恩0707 阅读(1927) 评论(1) 编辑 收藏 举报阅读目录
一:什么是后台同步保证离线功能呢?
在我们做移动端开发也好,还是做PC端应用也好,我们经常会碰到填写表单这样的功能,如果我们的表单填写完成以后,我们点击提交,但是这个时候我突然进入了电梯,或者我们在高铁上做这么一个操作,突然断网了,或者说我们的网络不好的情况下,那么一般的情况下会一直请求,当我们的请求超时的时候就会请求失败,或者说请求异常,最后就会提示我们网络异常这些信息,那么这样对于用户体验来说并不是很好,那么现在我们来理解下什么是后台同步保证离线功能呢?后台同步离线功能就是说当我们的网络不好的时候,我们点击提交按钮时,我们会保证该应用一定是成功的,不会提示网络异常这些信息,当网络连接失败的时候,我们会在前端页面显示一个提示,比如说,正在请求中,请稍微.... 这样的一个提示,当我们的网络恢复正常了,我们会重新去请求下该接口,那么我们这个应用就提示操作成功状态了。
后台同步:它使得我们能够确保用户采取的任何操作都能完成,不管用户的链接状态如何,甚至当用户点击提交后,直接关闭我们这个应用,不再回来,并且关闭浏览器,后台同步操作也能够完成。
后台同步的优点:
1. 对于用户而言,能够信任我们的渐进式web应用能一直工作,这也意味者我们与传统的web开发是有区别的,我们可以实现原生应用类似的效果。
2. 对于企业来讲,让用户在链接失败的时候,也能够订火车票,订阅新闻或发送消息,对于这样的用户体验也会更好。
二:后台同步是如何实现的呢?
后台同步原理的实质是:它是将操作从页面上下文中剥离开来,并且在后台运行。
通过将这些操作放到后台,它就不会受到单个网页的影响,即使网页被关闭,用户连接会断开,甚至服务器有时候会出现故障,但是只要我们电脑上安装了浏览器,后台同步的操作就不会消失,直到它成功完成为止。
1. 注册一个同步事件
使用后台同步很简单,我们首先要注册一个同步事件,如下代码:
navigator.serviceWorker.ready.then(function(registration) { registration.sync.register('send-messages'); });
如上代码可以在页面上运行,它获取了当前激活的service Worker 的 registration 对象,并注册了一个叫 send-messages 的sync事件。
现在,我们就可以将一个监听该同步事件的事件监听器添加到 service worker 中,该事件包含的逻辑将会在service worker 中执行,而不是在页面上执行的。
如下代码:
self.addEventListener("sync", function(event) { if (event.tag === "send-messages") { event.waitUntil(function(){ var sent = sendMessages(); if (sent) { return Promise.resolve(); } else { return Promise reject(); } }) } });
2. 理解 SyncManager
我们上面已经注册了一个sync事件,并且在service worker中监听了该sync事件。
那么所有与sync事件的交互都是通过 SyncManager 来完成的。SyncManager是 service worker 的一个接口。它可以让我们注册sync事件,并且我们可以获取已经注册的sync事件列表。
访问 SyncManager
我们可以通过已经激活的service worker的registration对象来访问 SyncManager, 在service worker里面,我们可以通过调用navigator.serviceWorker.ready 来访问当前激活的 service worker 的 registration对象,该方法会返回一个Promise对象,当成功时候我们可以拿到service worker的registration对象。
如下代码:
navigator.serviceWorker.ready.then(function(registration){});
如上代码,我们已经获得到了 registration 对象后,不管我们是在service worker上还是在页面上,和SyncManager交互现在都是一样的。
3. 注册事件
想要注册 sync事件,我们可以在 SyncManager中调用 register, 传入一个我们想要注册的 sync事件名称。
比如我们想注册一个在service worker中叫 send-message的事件,我们可以使用如下代码:
self.registration.sync.register("send-message");
如果我们想在service-worker中想做一样的事情的话,我们可以使用如下代码:
navigator.serviceWorker.ready.then(function(registration) { registration.sync.register("send-message"); });
4. 理解Sync事件原理
SyncManager 维护了一个Sync事件标签列表,SyncManager只知道哪些事件被注册了,何时被调用,以及如何发送sync事件。
当我们下面任何一个事件发生的时候,SyncManager会给列表中的每一个注册事件名发送一个sync事件。
1) sync事件注册后会立即发送。
2)当用户离线变成在线的时候也会发送。
3)如果还有未完成的事件时,每隔几分钟会发送。
在service worker中,我们发送sync事件,那么该事件就可以被监听到,并且我们可以使用promise进行响应,如果我们的这个promise完成了,那么对应的sync注册会从 SyncManager中删除,如果promise拒绝了,那么我们的sync注册的事件就会保留在SyncManager中,并且每隔几分钟会在下一个同步机会进行重试。
5. 理解 sync事件中的事件名称
sync事件中的事件名称是唯一的。如果在SyncManager中使用一个已经被注册的事件名称继续来注册的话,那么SyncManager 会忽略它,比如说:我们正在构建一个邮件服务,每当用户发送消息的时候,我们可以把消息保存到 indexedDB的发件箱中,并且注册一个send-email-message这样的后台同步事件,那么我们的service worker可以包含一个事件监听器进行监听,它会遍历indexedDB发件箱中的每一条消息,尝试发送他们,并且当发送成功后,将会从 indexedDB队列中删除它,如果我们当中有某条消息并没有发送成功的话,那么该sync事件就会被拒绝,SyncManager将会在稍后再次发送该事件,但是该事件是我们上次事件中发送失败的那个事件。使用这种设置,我们永远不需要检查发件箱中是否存在消息,只要有未发送的电子邮件,sync事件就会保持注册,并且尝试清空我们发的发件箱。
5. 理解获取已经注册的sync事件列表
我们使用SyncManager的getTags()方法,就可以得到完整的已注册同步事件列表。该getTags()方法也会返回一个Promise对象,该promise对象完成后,会获得一个sync注册事件名称的数组。
在service-worker中,我们可以注册一个叫 hello-world的sync事件,然后将当前注册的完整事件列表打印在控制台中中;如下代码:
self.registration.sync.register("hello-world").then(function() { return self.registration.sync.getTags(); }).then(function(tags) { console.log(tags); });
在我们的service worker中,我们首先通过使用 ready 获取 registration对象,也可以获取一样的结果,如下代码所示:
navigator.serviceWorker.ready.then(function(registration){ registration.sync.register('hello-world').then(function() { return registration.sync.getTags(); }).then(function(tags) { console.log(tags); }); });
6. 最后一次发生sync事件
在有些情况下,SyncManager可以会判断出尝试发送的sync事件已经多次失败,当发生这种情况的时候,SyncManager将会发送最后一次事件,给我们最后一次响应的机会,我们可以通过sync事件的lastChance属性来判断什么时候会发生这种情况,如下代码:
self.addEventListener("sync", event => { if (event.tag === "hello-world") { event.waitUntil( // 调用 addReservation方法 addReservation().then(function(){ return Promise.resolve(); }).catch(function(error) { if (event.lastChance) { return removeReservation(); } else { return Promise.reject(); } }) ) } })
三:如何给sync事件传递数据?
在页面接口交互中,我们可能需要传递一些参数进去,比如说,发生一个消息的接口中,可能我们需要把消息文本发送过去,一个为帖子点赞的接口,我们需要把帖子id的参数传递过去。但是当我们注册sync事件时,我们目前来看,我们之前只能传递事件名称,但是我们如何把一些对应的参数也传递给sync中的事件当中呢?
1. 在indexedDB中维护操作队列
要想把一些参数传递过去,我们可以把这些参数先保存到我们的indexedDB中,然后,我们在service worker中的sync事件代码我们可以迭代该对象存储,并且在每个条目上执行所需的操作,一旦操作成功了,我们就可以把该实体从对象存储中删除掉。
现在我们来做个demo,我们现在需要把每一条消息可以添加到 message-queue对象存储中,然后我们注册一个 send-message 后台同步事件来处理,该事件会遍历 message-queue对象中的所有消息,依次将他们发送到网络中,如果当所有消息都发送成功的话,我们会依次将消息队列中的数据删除,因此对象存储就为空了。但是如果有任何一条消息没有发送成功的话,就会向sync事件返回一个拒绝的promsie,SyncManager在稍后一段时间内会再次运行该sync事件。
如果我们之前使用如下代码,来请求一个接口,如下代码所示:
var sendMessage = function(subject, message) { fetch('/new-message', { method: 'post', body: JSON.stringify({ subject: subject, msg: message }) }) };
现在我们使用service worker,需要把代码改成如下所示:
var triggerMessageQueueUpdate = function() { navigator.serviceWorker.ready.then(function(registration) { registration.sync.register("message-queue-sync"); }); }; var sendMessage = function(subject, message) { addToObjectStore("message-queue", { subject: subject, msg: message }); triggerMessageQueueUpdate(); };
然后我们需要在service worker中监听sync事件代码如下:
self.addEventListener("sync", function(event) { if (event.target === 'message-queue-sync') { event.waitUntil(function() { return getAllMessages().then(function(messages) { return Promise.all( messages.map(function(message) { return fetch('/new-message', { method: 'post', body: JSON.stringify({ subject: subject, msg: message }) }).then(function(){ return deleteMessageFromQueue(message); }) }) ) }) }) } });
如上改写后的代码,首先我们会调用 addToObjectStore 这个方法来把消息保存到我们的key为 'message-queue' 当中,然后调用 triggerMessageQueueUpdate 这个方法,使用sync注册message-queue-sync这个事件,并且我们使用sync监听了该事件名称,然后我们使用了 getAllMessages 方法获取indexedDB的消息队列中的所有消息,并且最终返回了一个promise给sync事件,在该代码中,我们使用了Promise.all方法,在该方法内部,只有我们的消息发送成功后,我们才会使用 deleteMessageFromQueue方法来删除该消息,在我们的消息数组中,我们使用了map()方法遍历为每条消息发送一个promise对象.
2. 在indexedDB中维护请求队列
有时候在我们的项目中,我们需要实现本地存储架构来对对象状态进行跟踪,如果页面上有多个ajax请求的话,我们可以使用service worker 在indexedDB中来维护请求队列,我们可以将网络上的每个请求存储到indexedDB中,然后该方法会注册一个sync事件,该事件会遍历对象存储中所有请求,并依次执行。
比如项目中有如下代码:
var sendMessage = function(subject, message) { fetch('/new-message', { method: 'post', body: JSON.stringify({ subject: subject, msg: message }) }) }; var getRequest = function(id) { fetch('/like-post?id='+id); };
如上两个请求,我们使用service worker 换成如下代码:
var triggerRequestQueueSync = function() { navigator.serviceWorker.ready.then(function(registration){ registration.sync.register("request-queue"); }); }; var sendMessage = function(subject, message) { addToObjectStore("request-queue", { url: '/new-message', method: 'post', body: JSON.stringify({ subject: subject, msg: message }) }); triggerRequestQueueSync(); }; var getRequest = function(id) { addToObjectStore('request-queue', { url: '/like-post?id=' + id, method: 'get' }); triggerRequestQueueSync(); };
如上代码,我们将所有的网络请求替换成如上的代码,将代表请求对象存储到 request-queue的对象存储中,这个存储中每个对象代表一个网络请求,接下来我们需要添加一个sync事件监听器到service worker中,它负责遍历 request-queue的所有请求,依次会发起一个网络请求,发送成功后,依次从对象存储中删除。如下代码所示:
self.addEventListener("sync", function(event) { if (event.tag === "request-queue") { event.waitUntil(function(){ return getAllObjectsFrom("request-queue").then(function(requests) { return Promise.all( requests.map(function(req) { return fetch(req.url, { method: req.method, body: req.body }).then(function() { return deleteRequestFromQueue(req); // 返回一个promise }) }) ) }); }) } });
如上代码,如果一个请求发送成功了,就会从indexedDB中队列中删除掉,失败的请求会保留到队列中,并且返回被拒绝的promise,那么失败的promise会在请求队列的下一次sync事件中再次迭代。
3. 使用一种更简单的方式传递数据给事件名称
当我们需要传递一个简单的数据给sync函数时候,我们就不需要使用上面的indexedDB来存储数据,然后再service worker中依次遍历拿到该对象了,我们可以使用一种更简单的方式来解决如上的问题。我们之前的代码是这样的:
var likePost = function(postId) { fetch("/like-post?id="+postId); };
我们可以在service worker中使用如下代码来进行改造,如下代码所示:
var likePost = function(postId) { navigator.serviceWorker.ready.then(function(registration){ registration.sync.register("like-post-"+postId); }); };
我们使用sync事件来监听上面的函数,代码如下:
self.addEventListener("sync", function(event) { if (event.tag.startsWith("like-post-")) { event.waitUntil(function(){ var postId = event.tag.slice(10); return fetch('/like-post?id='+postId); }) } });
四:在我们的项目中添加后台同步功能
在我们项目中添加后台同步功能之前,我们还是来看下我们项目中的整个目录架构如下所示:
|----- service-worker-demo6 | |--- node_modules # 项目依赖的包 | |--- public # 存放静态资源文件 | | |--- js | | | |--- main.js # js 的入口文件 | | | |--- store.js # indexedDB存储 | | | |--- myAccount.js | | |--- styles | | |--- images | | |--- index.html # html 文件 | |--- package.json | |--- webpack.config.js | |--- sw.js
该篇文章是在上篇文章基础之上继续扩展的,如果想要看上篇文章,请点击这里
我们首先来看下我们 public/index.html 代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>service worker 实列</title> </head> <body> <div id="app">222226666</div> <img src="/public/images/xxx.jpg" /> <div style="cursor: pointer;color:red;font-size:18px;padding:5px;border:1px solid #333;" id="submit">点击我新增</div> <div style="cursor: pointer;color:red;font-size:18px;padding:5px;border:1px solid #333;" id="update">点击我修改</div> </body> </html>
public/js/myAccout.js 代码如下(该代码的作用最主要是做页面业务逻辑代码)
import $ from 'jquery'; $(function() { function renderHTMLFunc(obj) { console.log(obj); } function updateDisplay(d) { console.log(d); }; var addStore = function(id, name, age) { var obj = { id: id, name: name, age: age }; addToObjectStore("store", obj); renderHTMLFunc(obj); $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) { updateDisplay(data); }); }; $("#submit").click(function(e) { addStore(3, 'longen1', '111'); }); $("#update").click(function(e) { $.getJSON("http://localhost:8081/public/json/index.json", {id: 1}, function(data) { updateInObjectStore("store", 1, data); updateDisplay(data); }); }); });
如上myAccout.js代码,当我点击 id 为 "submit" 的div元素的时候(我们可以假设这是一个form表单提交,这边为了演示这个作用懒得使用form表单来演示),当我点击该div元素的时候,我们的addStore函数会被调用,这个函数内部会调用 addToObjectStore()这个方法,这个函数会添加一个store对象的存储,它会把该对象添加到IndexedDB的store对象中,添加完成以后,我们会调用renderHTMLFunc() 这个方法来渲染我们的html页面,并且之后我们会发起一个ajax请求。如果我们的网络一直是可以用的话,那么我们就不需要做任何处理操作,但是如果我们的网络连接失败的情况下,我们调用了 addStore 方法,那么我们新的数据会被添加到indexedDB中,并且会调用renderHTMLFunc方法来渲染我们的页面,但是后面的ajax请求就会调用失败。页面虽然更新了,indexedDB数据也保存到本地了,但是我们的服务器完全不知情,因此在这种情况下,我们需要使用service worker中的sync事件来解决这个问题。
我们要完成如下步骤:
1)在addStore函数添加代码,检查浏览器是否支持后台同步。如果支持,则注册一个 sync-store 同步事件,否则的话,便使用长规的ajax调用。
2)在store.js 中,添加到indexedDB代码,需要把状态改为 sending(发送中),在发送请求到服务器之前,这就是用户看到的状态,操作成功后,服务器会返回新的状态。
3)我们会向service worker添加一个事件监听器,用来监听sync事件,如果我们检测到sync的事件名称是 sync-store ,事件监听器就会遍历每一个处于 sending 状态的预订,并且尝试发送给服务器。成功添加到服务器之后,indexedDB中的状态就会被修改成为新的状态,如果任何服务器请求失败的话,那么整个sync事件就会被拒绝,浏览器就会尝试在随后再运行这个事件了。
因此我们现在的第一步是在 addStore函数中,添加浏览器是否支持同步功能,如果支持的话,就会注册一个sync事件。如下代码(在myAccount.js 代码修改):
var addStore = function(id, name, age) { var obj = { id: id, name: name, age: age }; addToObjectStore("store", obj); renderHTMLFunc(obj); // 先判断浏览器支付支持sync事件 if ("serviceWorker" in navigator && "SyncManager" in window) { navigator.serviceWorker.ready.then(function(registration) { registration.sync.register("sync-store") }); } else { $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) { updateDisplay(data); }); } };
因此我们的 public/js/myAccount.js 所有的代码如下:
import $ from 'jquery'; $(function() { function renderHTMLFunc(obj) { console.log(obj); } function updateDisplay(d) { console.log(d); }; var addStore = function(id, name, age) { var obj = { id: id, name: name, age: age }; addToObjectStore("store", obj); renderHTMLFunc(obj); // 先判断浏览器支付支持sync事件 if ("serviceWorker" in navigator && "SyncManager" in window) { navigator.serviceWorker.ready.then(function(registration) { registration.sync.register("sync-store").then(function() { console.log("后台同步已触发"); }).catch(function(err){ console.log('后台同步触发失败', err); }) }); } else { $.getJSON("http://localhost:8081/public/json/index.json", obj, function(data) { updateDisplay(data); }); } }; $("#submit").click(function(e) { addStore(3, 'longen1', '111'); }); $("#update").click(function(e) { $.getJSON("http://localhost:8081/public/json/index.json", {id: 1}, function(data) { updateInObjectStore("store", 1, data); updateDisplay(data); }); }); });
2)其次我们需要在 public/js/store.js 中openDataBase方法中的 result.onupgradeneeded 函数代码改成如下(当然要触发该函数,我们需要升级我们的版本号,即把 var DB_VERSION = 2; 把之前的 DB_VERSION 值为1 改成2):
var DB_VERSION = 2; var DB_NAME = 'store-data2'; // 监听当前版本号被升级的时候触发该函数 result.onupgradeneeded = function(event) { var db = event.target.result; var upgradeTransaction = event.target.transaction; var reservationsStore; /* 是否包含该对象仓库名(或叫表名)。如果不包含就创建一个。 该对象中的 keyPath属性id为主键 */ if (!db.objectStoreNames.contains('store')) { reservationsStore = db.createObjectStore("store", { keyPath: "id", autoIncrement: true }); } else { reservationsStore = upgradeTransaction.objectStore("store"); } if (!reservationsStore.indexNames.contains("idx_status")) { reservationsStore.createIndex("idx_status", "status", {unique: false}); } }
如上代码,我们在创建 store对象之前,我们会先判断该对象是否存在,如果不存在的话,我们会创建该对象,否则的话,我们就通过调用 event.target.transaction.objectStore("store")将获得更新事件中的事务,并且从事务中获取store对象存储的引用。
最后我们确认我们的store对象存储是否已经有 idx_status 这个,如果不存在的话,如果不存在的话,我们就创建该索引。
3)现在我们需要修改我们的 public/js/store.js 中的getStore函数了,我们在该函数内部使用这个新的索引,就可以获取到该某个请求中的某个状态了,因此我们要对 我们的 getStore函数进行修改,让其支持接收两个可选的参数,索引名称,以及传递给该索引的值。如下代码的修改:
var getStore = function (indexName, indexValue) { return new Promise(function(resolve, reject) { openDataBase().then(function(db) { var objectStore = openObjectStore(db, 'store'); var datas = []; var cursor; if (indexName && indexValue) { cursor = objectStore.index(indexName).openCursor(indexValue); } else { cursor = objectStore.openCursor(); } cursor.onsuccess = function(event) { var cursor = event.target.result; if (cursor) { datas.push(cursor.value); cursor.continue(); } else { if (datas.length > 0) { resolve(datas); } else { getDataFromServer().then(function(d) { openDataBase().then(function(db) { var objectStore = openObjectStore(db, "store", "readwrite"); for (let i = 0; i < datas.length; i++) { objectStore.add(datas[i]); } resolve(datas); }); }); } } } }).catch(function() { getDataFromServer().then(function(datas) { resolve(datas); }); }); }); };
如上代码,我们对getStore函数接受了两个可选的新参数(indexName和indexValue)。其次,如果我们的函数接收了这些参数的话,就使用参数在特定的索引(indexName)上打开流标,然后打开特定值(indexValue)的流标,会把结果限定在指定的范围内。如果没有传递这些参数的话,它会向以前那样运行。
做出这两个地方的修改,我们的函数可以返回所有的结果,也可以返回结果中的一个子集,如下代码所示:
getStore().then(function(reservations){ // reservations 包含了所有的数据 }); getStore("idx_status", "Sending").then(function() { // reservations 变量仅仅包含了状态为 "Sending" 的数据 });
4)现在我们需要在我们的 sw.js 中添加后台同步的事件监听器到 service worker中了。
首先我们在我们的sw.js中引入 store.js ,代码如下所示:
importScripts("/public/js/store.js");
然后在我们sw.js的底部,添加如下代码:
var createStoreUrl = function(storeDetails) { var storeUrl = new URL("http://localhost:8081/public/json/index.json"); Object.keys(storeDetails).forEach(function(key) { storeUrl.searchParams.append(key, storeDetails[key]); }); return storeUrl; }; var syncStores = function() { return getStore("idx_status", "Sending").then(function(reservations) { return Promise.all( reservations.map(function(reservation){ var reservationUrl = createStoreUrl(reservation); return fetch(reservationUrl); }) ) }); }; self.addEventListener("sync", function(event) { if (event.tag === "sync-store") { event.waitUntil(syncStores()); } });
如上代码,我们使用 self.addEventListener 为sync事件添加一个新的事件监听器,这个事件监听器会响应事件名称为 "sync-store" 的事件,然后使用 waitUntil方法等待 syncStores()函数返回的promise,会根据该promise的完成或拒绝来判断sync事件是完成还是拒绝,如果是完成的话,那么 sync-store的sync事件就会从SyncManager中删除,如果promise是拒绝的话,那么我们的SyncManager会保持 sync事件的注册,并且在随后会再次触发该事件。
syncStores() 会遍历IndexedDB中每一个标记为"Sending" 状态的数据,尝试会再次发送到服务器,并且返回一个promise,只有当promise发送成功了的话,那么就会完成状态。
然后在我们的getStore函数中,可以获取所有处于 Sending 状态的数据,该函数也返回了一个promise对象,该promise会决定整个syncStores函数的结果。要实现这点,我们使用了Promise.all()传入了一个promise数组,我们拿到该数据对象数组后,会通过Array.map()方法将数组的元素转化为 promise,我们使用 map对每个元素进行迭代,创建一个fetch请求发送到服务器来创建这个请求,fetch也会返回一个promise。
createStoreUrl 函数使用URL接口创建了一个新的URL对象,这个对象表示的是fetch请求接口,使用这种方式更优雅的创建带有查询字符串的URL,比如如下代码会打印带参数的url。
console.log(createStoreUrl({'name': 'kongzhi', 'age': 30}));
那么打印的 结构就是:http://localhost:8081/public/json/index.json?name=kongzhi&age=30; 这样的了。
完成上面的代码后,我们来打开我们的应用 http://localhost:8081/ 刷新下,然后我们把我们的网络断开,然后再点击 "点击我新增"这个文字,就会在控制台上打印如下信息了;如下图所示:
然后我们打开我们的网络,没过一会儿,就可以看到我们的请求会自动请求一次,如下图所示:
请求成功后,我们就可以把页面的消息 "请求加载中, 请稍后..." 这几个字 可以改成 "请求成功了..." 这样的提示了。
如上代码后,当我们的网络恢复完成后,我们会重新发ajax请求,请求完成后,可能会有新的请求状态数据,因此我们现在最后一步是需要更新我们的indexedDB数据库了,以便显示最新的消息给我们的用户,并且我们要更新我们的数据状态,等我们下一次 sync-store 事件注册的时候,不会重新发送。因此我们需要改变syncStores函数代码:
在更新之前,我们之前的代码是如下这样的:
var syncStores = function() { return getStore("idx_status", "Sending").then(function(reservations) { console.log(reservations); return Promise.all( reservations.map(function(reservation){ var reservationUrl = createStoreUrl(reservation); return fetch(reservationUrl); }) ) }); };
更新之后的代码如下所示:
var syncStores = function() { return getStore("idx_status", "Sending").then(function(reservations) { console.log(reservations); return Promise.all( reservations.map(function(reservation){ var reservationUrl = createStoreUrl(reservation); return fetch(reservationUrl).then(function(response) { return response.json(); }).then(function(newResponse) { return updateInObjectStore("store", 1, newResponse); }) }) ) }); };