-
Notifications
You must be signed in to change notification settings - Fork 823
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
V3 background sync #871
V3 background sync #871
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of the changes are around consuming bodies and needing to clone Request
s. See https://developer.mozilla.org/en-US/docs/Web/API/Request/clone
infra/testing/sw-env.js
Outdated
|
||
this.url = url; | ||
this.method = options.method || 'GET'; | ||
this.mode = options.mode || 'same-origin'; // FF defaults to cors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on the context, but for just a random new Request()
that you construct from code run in a window
client, it looks like Chrome will default to CORS, too:
r = new Request('.'); console.log(r.mode)
cors
Maybe just switch the default to 'cors'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll do that.
infra/testing/sw-env.js
Outdated
return new Request(this.url, this); | ||
} | ||
|
||
text() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The consume body algorithm is relevant here.
I'd expect this code to check bodyUsed
:
- If it's
true
, then reject withTypeError
. - If it's
false
, then set it to true and resolve withbody
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
infra/testing/sw-env.js
Outdated
} | ||
} | ||
|
||
clone() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the spec, and similar to the comment about the text()
method, this should take bodyUsed
into account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
infra/testing/sw-env.js
Outdated
this.mode = options.mode || 'same-origin'; // FF defaults to cors | ||
this.headers = new Headers(options.headers); | ||
|
||
this.bodyUsed = !!options.body; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're misinterpreting bodyUsed
. See my comments on clone()
and text()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* later. All parts of the storing and replaying process are observable via | ||
* callbacks. | ||
* | ||
* @example |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been taking out inline @example
s from the v3 JSDocs. I think the idea was to go with a model of having runnable examples on Glitch/etc. so there's one thing to keep up to date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
const serializableProperties = [ | ||
'method', | ||
'body', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think body
should be in this list. (See various other comments.)
limitations under the License. | ||
*/ | ||
|
||
const serializableProperties = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming that this list comes from https://fetch.spec.whatwg.org/#request
Have you confirmed that all those properties actually can be set when calling new Request()
?
And something like signal
seems like it would be difficult to correctly (de)serialize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't confirmed, but I only include these in the requestInit
dict if they're present.
But, either way, yeah, it probably doesn't make sense to include properties that are only handled by the UA. I'll look into this further.
* new Request object. A timestamp is also set in the event that when this | ||
* object was created is relevant after it's stored. | ||
* | ||
* @param {StorableRequest|Object} param1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use options
instead of param1
.
* new Request object. A timestamp is also set in the event that when this | ||
* object was created is relevant after it's stored. | ||
* | ||
* @param {StorableRequest|Object} param1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow this JSDoc—what's {StorableRequest|Object}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means it could be of either type. In this case a StorableRequest
object could be created from a plain object or "cloned" from another StorableRequest
instance.
However, looking at the code, I think I only create this from plain objects, so I can remove.
* defaulting to the current time if not specified. | ||
*/ | ||
constructor({url, requestInit, timestamp = Date.now()}) { | ||
this.url = url; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matt's been pretty keen on prefixing all instance variable names with _
as that apparently helps with our uglification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Unless you expect people to use instance.url
but even then, in some situations getters and setters can be smaller when using the private variable lots inside the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be unprefixed as it's public and can/must be editable by the user. E.g. workbox-google-analytics
will need to modify these values before requests are replayed.
infra/testing/sw-env.js
Outdated
const mockFetch = require('./mock-fetch'); | ||
|
||
Object.assign(global, makeServiceWorkerEnv()); | ||
global.self = global; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you assigning global to global.self
? makeServiceWorkerEnv has a self
property which you'll now we overriding with everything node has.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was necessary to handle the mocks we're adding later. I could try doing the global assignment after adding the mocks to see if that works. I remember there being an issue that this solve, but I can't remember all the various things I tried.
infra/testing/sw-env.js
Outdated
const swEnv = makeServiceWorkerEnv(); | ||
// Stub missing/broken Headers API methods in `service-worker-mock`. | ||
// https://fetch.spec.whatwg.org/#headers-class | ||
class Headers { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move all the mocks into a seperate file to clean up this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, the end goal was to PR this back into service-worker-mock
.
I can move it, but regardless this was always supposed to be a temporary solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
infra/testing/sw-env.js
Outdated
} | ||
}; | ||
|
||
global.__removeAllEventListeners = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename to __resetAllEventListeners. Would also be good if you move this to a seperate file and rather than expose this method on global, expose a function from the new file can all tests to call it (This is what the indexedDB module does - feels a little cleaner).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, though similar to my above comment, I was going to feature request this for the original library. We can discuss whether it makes sense to spend time on this now or spend time on a PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
infra/testing/sw-env.js
Outdated
|
||
// Needed to look like a real SW env |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an FYI - when you rebase, these next few lines will be removed (due to fix in service-worker-mock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, rebased.
|
||
|
||
const {indexedDBHelper, WorkboxError} = _private; | ||
const names = new Set(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like an unusual pattern - I think you are doing it to ensure a common set of names across all instances of Queue. Would be good to add a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? Using a set rather than an array?
* @param {Object} [param2] | ||
* @param {number} [param2.maxRetentionTime = 7 days] The amount of time (in | ||
* ms) a request may be retried. After this amount of time has passed, | ||
* the request will be deleted and not retried. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit .: reword to "deleted from the queue." insteand of "deleted and not retried."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I think the code could do with breaking up to simplify some of the flows of logic.
Separating out the QueueModel from the Sync logic should make it a bit easier to follow when / how sync events are / should be registered. Looking at this I'm unsure if this supports non-background-sync browsers.
* @param {number} [param2.maxRetentionTime = 7 days] The amount of time (in | ||
* ms) a request may be retried. After this amount of time has passed, | ||
* the request will be deleted and not retried. | ||
* @param {Object} [param2.callbacks] Callbacks to observe the lifecycle of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to plugins. That's the term used in the module names and in other parts of the API.
* | ||
* @private | ||
*/ | ||
_addSyncListener() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed in the previous team meeting that event listeners would be registered in the default export (i.e. the export that "does the right thing"). This is largely to ensure the logic is seperate from "magic".
Is there anyway of achieving that same line here?
See precaching and routing modules for examples
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand. Can you give more context?
Looking at those examples, they both expose a single instance of a class, whereas background sync exposes the class itself, and it needs a unique listener per instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/GoogleChrome/workbox/blob/v3/packages/workbox-routing/default-export.mjs#L22 is an example of what I did for the Router
class.
I had trouble wrapping my head around it as well, but once you get a feel for how the default
exports are likely be used in the context of workbox-sw
, it makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't think this applies to the background sync case since I don't instantiate anything in the default export.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That should be okay by me, let's see if @gauntface has any concerns. He's given this setup a lot more thought that I have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have concerns but this module is also too big to cover off everything. I'll play around with it and see if it makes to refactor after.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'd be happy to look at a suggestion for how it could be refactored, but since I don't understand what you're asking me to do, I can't really do it myself.
* | ||
* @return {Object} | ||
*/ | ||
createPlugin() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the use case for this function?
The RequestWrapper is non-existant now and even then, I'm struggling to see how this would be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because it was in the original API design. Here's the original code example (modified to use createPlugin()
):
const queue = new workbox.backgroundSync.Queue('myQueue');
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
plugins: [queue.createPlugin()],
});
const route = new workbox.routing.RegExpRoute({
regExp: new RegExp('^https://jsonplaceholder.typicode.com'),
handler: new workbox.runtimeCaching.NetworkOnly({requestWrapper}),
});
const router = new workbox.routing.Router();
router.registerRoute({route});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd drop this in favor of exposing a plugin class like the other plugins. While I understand the convenience of this, nothing else follows this pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the v2 way (where the plugin class just extends the Queue
class), but if you want it to be a class instead of an object, it could be like this:
class QueuePlugin {
constructor(queue) {
this._queue = queue
}
fetchDidFail({request}) {
this._queue.addRequest(request);
}
}
And you'd use it like this:
new workbox.runtimeCaching.NetworkOnly({
plugins: new QueuePlugin(queue),
});
vs. how I have it currently:
new workbox.runtimeCaching.NetworkOnly({
plugins: queue.createPlugin(),
});
Alternatively, we could just do away with this feature entirely, since it's really not that hard to do this:
new workbox.runtimeCaching.NetworkOnly({
plugins: {
fetchDidFail: ({request}) {
queue.addRequest(request);
}
}
});
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd vote for the first option.
If we go with explicitly needing to know the lifecycle callback names to use this behavior, then the precedent would be to use that approach with all the other plugins as well.
That isn't a great user experience for developers who care about specifying behaviors, but don't care about the underlying lifecycle callbacks. It's also an issue for the other behaviors that require multiple callbacks (I think cache expiration does?), since it's even harder to properly configure that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM. Done.
* @param {...*} args The arguments to invoke the callback with. | ||
*/ | ||
_runCallback(name, ...args) { | ||
if (typeof this._callbacks[name] == 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
triple equals please. We aren't in closure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. But what about the bytes Matt! The BYTES!
try { | ||
await this._waitUntilActive(); | ||
await registration.sync.register(`${TAG_PREFIX}:${this._name}`); | ||
} catch (err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it does fail, I'd still log an error or warning in dev builds.
it(`should try to replay the queue on SW startup in browsers that ` + | ||
`don't support the sync event`, async function() { | ||
// Delete the SyncManager interface to mock a non-supporting browser. | ||
const originalSyncManager = registration.sync; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you might be able to do sanbox.stub(registration, 'sync').value(undefined)
and that you don't need to track original or perform delete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I tried that as well as .value()
(leaving it empty) and neither worked. Probably a good feature request for sinon.
|
||
expect(Queue.prototype.replayRequests.calledOnce).to.be.true; | ||
|
||
registration.sync = originalSyncManager; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the expect fails and throws, the original will never be set. Might be best to store the original in the describe and re-set in beforeEach and after (if the sinon approach doesn't work)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the tests fail, do we care if state gets messed up? I could also do something like this:
try {
expect(Queue.prototype.replayRequests.calledOnce).to.be.true;
} catch (err) {
throw err;
} finally {
registration.sync = originalSyncManager;
}
}); | ||
|
||
describe(`createPlugin`, function() { | ||
it(`should return an object implementing the fetchDidFail plugin ` + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method still feels weird to me. May be naming, but I'm struggling to understand what it's for.
await queue.addRequest(request); | ||
|
||
// Allow time to ensure async registration didn't happen. | ||
await sleep(100); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels flakey and unnecessary. This doesn't seem to affect the flow at all of this test. If you do need timeouts please use sinon fake timers.
it(`should ignore (and remove) requests if maxRetentionTime has passed`, | ||
async function() { | ||
sandbox.spy(self, 'fetch'); | ||
const clock = sinon.useFakeTimers({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you get this from the sandbox? If so you can drop the restore call at the end of this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't in the documentation, but yep, appears to work. Done.
110e08b
to
ffd750d
Compare
bc69222
to
122eca3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gauntface, @jeffposnick PTAL.
Changes since your last reviews:
- Per Matt's suggestion: added a
QueueStore
module to break upQueue
logic. - Per Matt's suggestion: removed the
requestDidFail
andrequestDidReplay
callbacks and changedallRequestsDidReplay
toqueueDidReply
but now it runs anytime the queue is retried (previously it was only on success) - Added support for Blobs in requests
- Removed the use of
indexedDBHelper
since I decided to use an index instead ofgetAll()
. - Per Matt's suggestion: moved sw-env mocks into their own files
- Per Matt's suggestion: removed the global
__removeAllEventListeners
in favor of exporting aresetEventListeners
function from the event listener mock.
Note: I'll update #868 with changes after we agree on changes and merge.
a927efa
to
76b650e
Compare
const requestInit = {headers: {}}; | ||
|
||
// Set the body if present. | ||
if (request.method != 'GET') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you switch here and elsewhere to !==
? (Shouldn't our linting pick that up?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the linting only warns against double-equals comparison with falsy values (as that's the only time it gets tricky).
I can change here, but I think we should add this to our style guide if we're going to enforce it, as it's not part of Google JS style.
* | ||
* @private | ||
*/ | ||
_addSyncListener() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/GoogleChrome/workbox/blob/v3/packages/workbox-routing/default-export.mjs#L22 is an example of what I did for the Router
class.
I had trouble wrapping my head around it as well, but once you get a feel for how the default
exports are likely be used in the context of workbox-sw
, it makes more sense.
global.fetch = mockFetch; | ||
// Assign all properties of `self` to `global`; | ||
Object.assign(global, serviceWorkerMock()); | ||
global.self = global; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still confused at why this self is needed. serviceWorkerMock creates a self and this will overwrite that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not necessarily needed, but if we don't do this then all the code below would need to be like this:
global.Blob = global.self.Blob = Blob;
global.Event = global.self.Event = Event;
// ...
So I guess it's a matter of which we prefer. I think what I have is fine since it's actually the same way browsers implement it, i.e. self (and window) is a property of the global object that points to the global object. For example: window.window === window
, window.self === window
, self.self === self
, etc.
* in IndexedDB specific to this instance. An error will be thrown if | ||
* a duplicate name is detected. | ||
* @param {Object} [param2] | ||
* @param {Object} [param2.callbacks] Callbacks to observe the lifecycle of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it fair to say that these callbacks are the same as plugins? If so can we rename this to plugins rather than callback for consistency throughout workbox.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think they're the same. As far as I'm aware, a plugin in an object that implements lifecycle callbacks, and this doesn't do that.
I've debated with myself as to whether or not these should be events and the Queue
class inherit from something like EventEmitter
, but I didn't want to do that in this PR to keep things as close to the original as possible.
* @param {Object} [param2.callbacks] Callbacks to observe the lifecycle of | ||
* queued requests. Use these to respond to or modify the requests | ||
* during the replay process. | ||
* @param {function(StorableRequest):undefined} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the syntax for :undefined
? The parameter is marked as optional given the square brackets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means the function returns undefined
. The syntax is described here: https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System
But that brings up a good point that this is closure compiler jsdoc notation, and I'm not sure if what we use to generate docs will support this. I don't think non-CC-jsdoc syntax has a way to specify what params a function takes, what its this
binding is, or what it returns (which is too bad, IMO).
* | ||
* @return {Object} | ||
*/ | ||
createPlugin() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd drop this in favor of exposing a plugin class like the other plugins. While I understand the convenience of this, nothing else follows this pattern.
* | ||
* @private | ||
* @param {string} name The name of the callback on this._callbacks. | ||
* @param {...*} args The arguments to invoke the callback with. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you confirm this works in JSDocs with gulp docs
* | ||
* @private | ||
*/ | ||
_addSyncListener() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have concerns but this module is also too big to cover off everything. I'll play around with it and see if it makes to refactor after.
* | ||
* @private | ||
*/ | ||
async _registerSync() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming calling sync with the same details more than once get's de-duped by the browser?
PR-Bot Size PluginChanged File Sizes
New Files
All File SizesView Table
Workbox Aggregate Size PluginTotal Size: 9.31KB Gzipped: 4.33KB |
R: @jeffposnick @addyosmani @gauntface
Changes described in #868.