Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 Background Sync API updates #1710

Merged
merged 4 commits into from
Nov 2, 2018
Merged

v4 Background Sync API updates #1710

merged 4 commits into from
Nov 2, 2018

Conversation

philipwalton
Copy link
Member

@philipwalton philipwalton commented Oct 15, 2018

R: @jeffposnick

Addresses #1424, #1502, #1503, #1637, and #1695.

Based on lots of feedback in the above issues/PRs, its clear that the callbacks provided in workbox.backgroundSync.Queue are not sufficient to handle many real-world background sync needs.

This PR attempts to solve this issue by exposing more low-level features of the queue, which should give developers all the flexibility they need; the downside is it may make very simple replay modifications a bit more complicated. Hopefully (due to the complex nature of background sync in general), this downside will affect very few people.

Current API:

In Workbox v3, the workbox.backgroundSync.Queue class had the following public methods:

  • addRequest()
  • replayRequests()

And the following replay callbacks that allowed you modify requests as the queue was replaying:

  • requestWillEnqueue
  • requestWillReplay
  • queueDidReplay

But if you wanted to do something other than simply replay every single request, it was very difficult if not impossible. For example, you couldn't easily:

  • Update a request based on the response from the previous request.
  • Remove an individual request from the queue or otherwise prevent it from sending.
  • Send the queued requests in a different order.

Proposed changes:

To support the above use cases, the changes in this PR remove the callbacks object and replace it with a single onSync callback. The onSync callback will be invoked whenever the queue receives a sync event (or at SW startup in non-sync browsers), and if you don't specify an onSync callback, the default behavior is to run replayRequests() like in v3 (but without invoking the v3 callbacks).

The onSync callback is invoked with the queue instance as its only argument, allowing you to handle the queue however you want in response to a sync event.

In addition, the workbox.backgroundSync.Queue class has removed the addRequest() method, and replaced it with four array-like methods to make it easier to manage queued requests manually.

  • pushRequest()
  • popRequests()
  • shiftRequest()
  • unshiftRequests()

The push and unshift methods take an object in the form {request, [metadata], [timestamp]} (only request is required), and the shift and pop methods return an object in exactly the same form.

The optional metadata property allows you to store data associated with the request in the queue that may be relevant when re-fetching the request later.

The optional timestamp property defaults to the current time, and it affects when requests expire. Normally you don't need to pass this, but it can be useful if you want to explicitly reset the expiry time for a particular request in order to keep it in the queue longer.

Example onSync callback

To replay all requests in the queue after a sync event, you could do the following:

const queue = new workbox.backgroundSync.Queue('my-queue-name', {
  onSync: async (queue) => {
    let entry;
    while (entry = await this.shiftRequest()) {
      try {
        await fetch(entry.request);
        console.error('Replay successful for request', entry.request);
      } catch (error) {
        console.error('Replay failed for request', entry.request, error);

        // Put the entry back in the queue and re-throw the error:
        await this.unshiftRequest(entry);
        throw error;
      }
    }
    console.log('Replay complete!');
  }
});

Note: when using an async function for the onSync callback (recommended) the sync event automatically waits for the function to complete, so you don't have to manually call event.waitUntil().

Using the Plugin class

Since the plugin class mostly just shadows the Queue class, you can pass the onSync callback to plugin instances in exactly the same way.

Note that when using the plugin, the onSync callback is still invoked with a Queue instance, so you don't have to create a reference to the queue yourself.

new workbox.backgroundSync.Plugin('my-queue-name', {
  onSync: async (queue) => {
    // queue is an instance of `Queue` not `Plugin`.
  }
});

Open questions

By removing the callback object in favor of a single onSync callback, one thing that is harder to accomplish is notifying of a successful queue replay in situations where you don't want to modify the replay logic.

With the changes in this PR, it's still possible, it's just perhaps not as intuative as before. Here's what you'd have to do:

const queue = new workbox.backgroundSync.Queue('my-queue-name', {
  onSync: async (queue) => {
    await queue.replayRequests();

    // The replay was successful! Notification logic can go here.
    console.log('Replay complete!');
  }
});

If you want to notify of success and failure, you can use a try/catch block:

const queue = new workbox.backgroundSync.Queue('my-queue-name', {
  onSync: async (queue) => {
    try {
      await queue.replayRequests();
      
      // The replay was successful! Notification logic can go here.
      console.log('Replay complete!');
    } catch (error) {
      // The replay failed...
      console.log(error);
    }
  }
});

Since the above examples are both pretty simple, I'm leaning toward not having onsuccess or onfailure callbacks like before, but if enough people want them, I could be persuaded otherwise.

@bennypowers
Copy link

What about some means of mapping over the queue?

Copy link
Contributor

@jeffposnick jeffposnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally 👍 with the overall approach! I did have a few comments about the public interface, though.

packages/workbox-background-sync/Queue.mjs Outdated Show resolved Hide resolved
packages/workbox-background-sync/Queue.mjs Outdated Show resolved Hide resolved
@workbox-pr-bot
Copy link
Collaborator

PR-Bot Size Plugin

Changed File Sizes

File Before After Change GZipped
packages/workbox-background-sync/build/workbox-background-sync.prod.js 3.51 KB 3.60 KB +3% 1.56 KB
packages/workbox-broadcast-cache-update/build/workbox-broadcast-cache-update.prod.js 1.12 KB 1.88 KB +68% 940 B ☠️
packages/workbox-build/build/index.js 4.02 KB 3.64 KB -9% 1.36 KB
packages/workbox-cache-expiration/build/workbox-cache-expiration.prod.js 3.88 KB 3.15 KB -19% 1.29 KB 🎉
packages/workbox-cacheable-response/build/workbox-cacheable-response.prod.js 587 B 595 B +1% 351 B
packages/workbox-cli/build/app.js 6.76 KB 4.64 KB -31% 1.69 KB 🎉
packages/workbox-cli/build/bin.js 2.32 KB 1.16 KB -50% 580 B 🎉
packages/workbox-core/build/workbox-core.prod.js 7.47 KB 6.09 KB -18% 2.76 KB 🎉
packages/workbox-navigation-preload/build/workbox-navigation-preload.prod.js 660 B 668 B +1% 323 B
packages/workbox-precaching/build/workbox-precaching.prod.js 5.81 KB 5.03 KB -13% 2.12 KB 🎉
packages/workbox-range-requests/build/workbox-range-requests.prod.js 1.63 KB 1.50 KB -8% 750 B
packages/workbox-routing/build/workbox-routing.prod.js 2.87 KB 3.24 KB +13% 1.41 KB ☠️
packages/workbox-strategies/build/workbox-strategies.prod.js 5.09 KB 4.75 KB -7% 1.19 KB
packages/workbox-streams/build/workbox-streams.prod.js 1.57 KB 1.39 KB -11% 684 B 🎉
packages/workbox-sw/build/workbox-sw.js 1.50 KB 1.51 KB +1% 819 B
packages/workbox-webpack-plugin/build/generate-sw.js 8.04 KB 5.29 KB -34% 1.84 KB 🎉
packages/workbox-webpack-plugin/build/index.js 742 B 349 B -53% 255 B 🎉
packages/workbox-webpack-plugin/build/inject-manifest.js 10.30 KB 6.91 KB -33% 2.40 KB 🎉

New Files

File Size GZipped
packages/workbox-google-analytics/build/workbox-offline-ga.prod.js 1.74 KB 878 B

All File Sizes

View Table
File Before After Change GZipped
packages/workbox-background-sync/build/workbox-background-sync.prod.js 3.51 KB 3.60 KB +3% 1.56 KB
packages/workbox-broadcast-cache-update/build/workbox-broadcast-cache-update.prod.js 1.12 KB 1.88 KB +68% 940 B ☠️
packages/workbox-build/build/_types.js 41 B 41 B 0% 61 B
packages/workbox-build/build/index.js 4.02 KB 3.64 KB -9% 1.36 KB
packages/workbox-cache-expiration/build/workbox-cache-expiration.prod.js 3.88 KB 3.15 KB -19% 1.29 KB 🎉
packages/workbox-cacheable-response/build/workbox-cacheable-response.prod.js 587 B 595 B +1% 351 B
packages/workbox-cli/build/app.js 6.76 KB 4.64 KB -31% 1.69 KB 🎉
packages/workbox-cli/build/bin.js 2.32 KB 1.16 KB -50% 580 B 🎉
packages/workbox-core/build/workbox-core.prod.js 7.47 KB 6.09 KB -18% 2.76 KB 🎉
packages/workbox-google-analytics/build/workbox-offline-ga.prod.js 1.74 KB 878 B
packages/workbox-navigation-preload/build/workbox-navigation-preload.prod.js 660 B 668 B +1% 323 B
packages/workbox-precaching/build/workbox-precaching.prod.js 5.81 KB 5.03 KB -13% 2.12 KB 🎉
packages/workbox-range-requests/build/workbox-range-requests.prod.js 1.63 KB 1.50 KB -8% 750 B
packages/workbox-routing/build/workbox-routing.prod.js 2.87 KB 3.24 KB +13% 1.41 KB ☠️
packages/workbox-strategies/build/workbox-strategies.prod.js 5.09 KB 4.75 KB -7% 1.19 KB
packages/workbox-streams/build/workbox-streams.prod.js 1.57 KB 1.39 KB -11% 684 B 🎉
packages/workbox-sw/build/workbox-sw.js 1.50 KB 1.51 KB +1% 819 B
packages/workbox-webpack-plugin/build/generate-sw.js 8.04 KB 5.29 KB -34% 1.84 KB 🎉
packages/workbox-webpack-plugin/build/index.js 742 B 349 B -53% 255 B 🎉
packages/workbox-webpack-plugin/build/inject-manifest.js 10.30 KB 6.91 KB -33% 2.40 KB 🎉

Workbox Aggregate Size Plugin

9.71KB gzip'ed (65% of limit)
23.79KB uncompressed

@philipwalton
Copy link
Member Author

What about some means of mapping over the queue?

@bennypowers, Jeff and I discussed something like this:

for await (const entry of queue.entries()) {
  try {
    await fetch(entry.request);
  } catch (error) {
    await this.unshiftRequest(entry);
    break;
  }
}

But unfortunately that would require async iterators, which aren't supported in Edge.

We could add a mapRequests() or forEachRequest() method as well, but that wouldn't allow you to easily break out of the loop if you need to.

queue.forEachRequest(async (entryPromise) => {
  // But calling `return` here won't exit the loop...
})

Is that what you were envisioning by your suggestion?

I think given the limitations of both options a while loop is still the simplest to implement at the moment.

@bennypowers
Copy link

bennypowers commented Oct 17, 2018 via email

@philipwalton
Copy link
Member Author

@Toilal could you please take a look at see if these changes work for the use case you brought up in #1502?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants