1

I need to make a POST request and send some data. I'm using the service worker sync to handle offline situation.

But is there a way to pass the POST data to the service worker, so it makes the same request again?

Cause apparently the current solution is to store requests in some client side storage and after client gets connection - get the requests info from the storage and then send them.

Any more elegant way?

PS: I thought about just making the service worker send message to the application code so it does the request again ... but unfortunately it doesn't know the exact client that registered the service worker :(

pesho hristov
  • 1,946
  • 1
  • 25
  • 43

2 Answers2

6

You can use fetch-sync

or i use postmessage to fix this problem, which i agree that indexedDB looks trouble.

  1. first of all, i send the message from html.

    
    // send message to serviceWorker
    function sync (url, options) {
      navigator.serviceWorker.controller.postMessage({type: 'sync', url, options})
    }
    
  2. i got this message in serviceworker, and then i store it.

    
    const syncStore = {}
    self.addEventListener('message', event => {
      if(event.data.type === 'sync') {
        // get a unique id to save the data
        const id = uuid()
        syncStore[id] = event.data
        // register a sync and pass the id as tag for it to get the data
        self.registration.sync.register(id)
      }
      console.log(event.data)
    })
    
  3. in the sync event, i got the data and fetch

    
    self.addEventListener('sync', event => {
      // get the data by tag
      const {url, options} = syncStore[event.tag]
      event.waitUntil(fetch(url, options))
    })
    

it works well in my test, what's more you can delete the memory store after the fetch

what's more, you may want to send back the result to the page. i will do this in the same way by postmessage.

  1. as now i have to communicate between each other, i will change the fucnction sync into this way

    
    // use messagechannel to communicate
    sendMessageToSw (msg) {
      return new Promise((resolve, reject) => {
        // Create a Message Channel
        const msg_chan = new MessageChannel()
    
    
    // Handler for recieving message reply from service worker
    msg_chan.port1.onmessage = event => {
      if(event.data.error) {
        reject(event.data.error)
      } else {
        resolve(event.data)
      }
    }
    
    navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2])
    
    }) } // send message to serviceWorker // you can see that i add a parse argument // this is use to tell the serviceworker how to parse our data function sync (url, options, parse) { return sendMessageToSw({type: 'sync', url, options, parse}) }
  2. i also have to change the message event, so that i can pass the port to sync event

    
    self.addEventListener('message', event => {
      if(isObject(event.data)) {
        if(event.data.type === 'sync') {
          // in this way, you can decide your tag
          const id = event.data.id || uuid()
          // pass the port into the memory stor
          syncStore[id] = Object.assign({port: event.ports[0]}, event.data)
          self.registration.sync.register(id)
        }
      }
    })
    
  3. up to now, we can handle the sync event

    
    self.addEventListener('sync', event => {
      const {url, options, port, parse} = syncStore[event.tag] || {}
      // delete the memory
      delete syncStore[event.tag]
      event.waitUntil(fetch(url, options)
        .then(response => {
          // clone response because it will fail to parse if it parse again
          const copy = response.clone()
          if(response.ok) {
            // parse it as you like
            copy[parse]()
            .then(data => {
              // when success postmessage back
              port.postMessage(data)
            })
          } else {
            port.postMessage({error: response.status})
          }
        })
        .catch(error => {
          port.postMessage({error: error.message})
        })
      )
    })
    

At the end. you cannot use postmessage to send response directly.Because it's illegal.So you need to parse it, such as text, json, blob, etc. i think that's enough.

As you have mention that, you may want to open the window. i advice that you can use serviceworker to send a notification.

self.addEventListener('push', function (event) {

  const title = 'i am a fucking test'
  const options = {
    body: 'Yay it works.',
  }
  event.waitUntil(self.registration.showNotification(title, options))
})
self.addEventListener('notificationclick', function (event) {

  event.notification.close()

  event.waitUntil(
    clients.openWindow('https://yoursite.com')
  )
})

when the client click we can open the window.

To-xic
  • 211
  • 2
  • 4
  • That looks very nice ;) ... One thing that I'm thinking about though - is there a way to `return` the result of this `fetch` request to the page? ... Cause sending message from service worker to app is possible, but you need to know the exact client who started the sync. And also - can we handle the situation of sending the response back to the page in such way: 1) if the page is closed - to open the page? 2) page is still opened - show some custom message with the response. – pesho hristov Feb 05 '17 at 15:48
  • i update the answer.it includes method to send back. We also can do some stuff when we send back the response.i will do the two thing you mentioned by using notifications. – To-xic Feb 06 '17 at 12:33
  • the notification part is not complete.if you are interested in it. you can google it. – To-xic Feb 06 '17 at 13:01
  • I'm stumbling upon issues, when I just use `client.postMessage()` in the service worker. Scenario: one-way communication from SW to app. Inside the promise chain links of `event.waitUntil()` in the sync event listener this leads to a non finishing Promise or something like this. Did you ever see this problem with your `port.postMessage(data)` (I know, that this a port from the main app though)? I guess I need to close the connection somewhere. – BairDev Apr 11 '17 at 09:51
  • **Regarding the last comment:** forget about it. It was a really stupid error in my new Promise parameter function (no call of `resolve()`. – BairDev Apr 11 '17 at 13:21
0

To comunicate with the serviceworker I use a trick:

in the fetch eventlistener I put this:

self.addEventListener('fetch', event => {
  if (event.request.url.includes("sw_messages.js")) {
    var zib = "some data";
    event.respondWith(new Response("window.msg=" + JSON.stringify(zib) + ";", {
      headers: {
        'Content-Type': 'application/javascript'
      }
    }));
  }

  return;
});

then, in the main html I just add:

<script src="sw_messages.js"></script>

as the page loads, global variable msg will contain (in this example) "some data".

Zibri
  • 9,096
  • 3
  • 52
  • 44