-
Notifications
You must be signed in to change notification settings - Fork 819
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
Deduplicate requests #2593
Comments
Hmm, that's interesting. I'd like to hear more about exactly how you envision this working, and whether caching comes into play at all, or if it's network-only. Here's what I think you're asking for: a custom In that model, there's no caches that come into play, and the pausing only happens if there is an actual overlap between two inflight requests; if two requests are made sequentially, without and overlap, then the second one would behave independent of the first. Does that sound like what you had in mind? |
I also wanted to note that, especially with the upcoming support for custom An ecosystem of third-party modules that worked with Workbox would be 💯 . |
That's exactly what I had in mind at first. Check this early-stage implementation (still need some work) https://github.com/ianldgs/workbox/blob/feat/dedup/packages/workbox-strategies/src/DedupRequests.ts. But then I realized that if I wanted to add offline support I wouldn't be able to use the For those reasons, I'm thinking that it might make more sense to have a plugin, so one could do: workbox.routing.registerRoute(
({ request }) => request.url.startsWith("/api"),
new workbox.strategies.NetworkFirst({
cacheName: "api-cache",
networkTimeoutSeconds: 5,
plugins: [
+ new DedupRequestsPlugin(),
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 2 * 24 * 60 * 60, // 2 days
}),
],
})
); But to have this, I would actually need to be able to override the
Yep, that would be great! Currently, the only way I was able to create the custom strategy was to clone the entire repository. |
CC: @philipwalton for his thoughts, as he did most of the Passing in a reference to the
I'd imagine that the way this should work while testing is for you to set up a
And then folks using your custom class (or plugin) would be responsible for installing it and |
FYI, I've done a POC on adding It allows me to write a plugin like the following: function DedupRequestsPlugin() {
/**
* @type {Map<string, Promise<Response | undefined>>}
*/
const requestsInProgress = new Map();
return {
/**
* @param {RequestInfo} request
* @param {RequestInit|undefined} options
*/
async fetch(request, options) {
const requestKey = `${request.method} ${request.url}`;
const requestInProgress = requestsInProgress.get(requestKey);
if (requestInProgress) return requestInProgress;
try {
const responsePromise = fetch(request, options);
requestsInProgress.set(
requestKey,
responsePromise.then((r) => r.clone())
);
const response = await responsePromise.then((r) => r.clone());
return response;
} finally {
requestsInProgress.delete(requestKey);
}
},
};
} And to use: workbox.routing.registerRoute(
({ request }) => request.url.startsWith("/api"),
new workbox.strategies.NetworkFirst({
cacheName: "api-cache",
networkTimeoutSeconds: 5,
plugins: [
+ DedupRequestsPlugin(),
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 2 * 24 * 60 * 60, // 2 days
}),
],
})
); Or actually use it with any strategy I want. |
I think you can do this fairly cleanly using a plugin—as long as you're OK with it only deduping requests using strategies that have added that plugin to them. Since all plugin callbacks are async, you should be able to await pending requests in the {
async handlerWillStart({request}) {
// 1. Add the URL to a global request map
// 2. If there's an existing request being processed,
// wait for it to finish.
await noPendingRequestsForURL(request.url)
}
async handlerWillRespond({request}) {
// Remove a URL from the global map once it's ready to respond.
clearURLFromRequestMap(request.url)
}
} Does that sound like it would work for your use case? |
I like this solution! Will try it out and if it works, I'll close this. |
Update: I've tried it but didn't manage to make it work. Posting the code I used in case you spot something I didn't. I also tried with the Is there any particular reason you wouldn't accept a PR to allow overriding function DedupRequestsPlugin() {
console.log("DedupRequestsPlugin");
/**
* Temporary cache for requests being done at the same time.
*
* @type {Map<string, { promise: Promise<any>, done: () => any }>}
*/
const requestsInProgress = new Map();
return {
/** @param context {{request: Request}} */
async handlerWillStart({ request }) {
console.log("requestWillFetch", request.url);
const requestInProgress = requestsInProgress.get(request.url);
if (requestInProgress) {
console.log("is in progress, waiting");
await requestInProgress.promise;
console.log("finished");
return request;
}
console.log("not in progress, acting normally");
let done;
requestsInProgress.set(request.url, {
promise: new Promise((resolve) => {
done = resolve;
}),
done,
});
console.log("added to in-progress cache");
return request;
},
/** @param context {{request: Request, response: Response}} */
async handlerWillRespond({ request, response }) {
console.log("fetchDidSucceed", request.url);
const requestInProgress = requestsInProgress.get(request.url);
if (requestInProgress) {
console.log("cleaning up");
requestInProgress.done();
requestsInProgress.delete(request.url);
}
console.log("ok");
return response;
},
};
} |
Ahh, right, that makes sense. If you use the
I'm not necessarily opposed to it, but it does make me a bit uncomfortable. I think we'd want to think a bit more about the potential uses (and abuses) before adding something like that. Before we go down that road I'd like to explore whether or not it's possible to do by either writing your own custom strategy (as @jeffposnick suggested above) or even extending whatever built-in strategy you're using. For example, you could extend the class DedupeCacheFirst extends CacheFirst {
async _handle(request, handler) {
// Check for a duplicate, pending request
if (isDuplicateRequestPending(request)) {
return await getResponseForDuplicateRequest(request);
}
return await super._handle(request, handler);
}
} Do you think that work would? |
I played around with this a bit today and got the following working:
You can try this out with a simple web page like:
Checking the Network panel and the logged messages, I think that gives the behavior that @ianldgs originally asked for. In that all the duplicate requests that are made while an original request is still in-flight will wait for the original response, and once the response is available, they'll all use a clone of it to fulfill their respective requests. (The little gear icon indicates that only one of the requests processed by the service worker actually resulted in an outgoing request to the network.) |
Hey @jeffposnick and @philipwalton, this looks really good, thanks a lot! I will go with this approach. It didn't look like the best approach to me after playing with it, because the
Fair enough! It does feel a bit weird, yes, since the I'm closing this since I'm pretty sure no changes are needed to meet the requirements I have. Have a nice week guys! |
Glad to hear it! @philipwalton, that's good feedback for when it's time to document extending |
Yeah, I agree it does make it look like you're not supposed to define it, but hopefully it'll be clear with good documentation. The idea is that the This is similar to Node's Readable Streams implementation, where when you create your own streams subclass you have to define the |
Library Affected:
workbox-core, workbox-strategies
Browser & Platform:
all browsers
Issue or Feature Request Description:
Support for deduplicating requests.
Or
Support for creating custom strategies
Or
Extend plugin API
Use case
2 API calls to the same endpoint, with the same query, being done at the same time
What I've got so far
I have cloned the repo (V6) and created a custom strategy. However, the structure is not being very helpful for this case. I cannot base my strategy on another strategy, or even use the strategy as secondary and it depends on a lot of internals.*
Idea
I do understand that, apart from the problems, this strategy wouldn't be as useful as the others currently provided and that creating custom strategies isn't something that people do every day. So the most elegant solution would probably be to provide a
fetch
method on the plugin API. TherequestWillFetch
doesn't work for this case.PS: If you like the idea, I can create a pull request. I've got a bit familiar with the project and internals.
The text was updated successfully, but these errors were encountered: