You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Workbox comes with a set of plugins and strategies that are very easy to use (and use together), but if a developer wants a slightly different behavior from what we provide out of the box, they often have to start from scratch.
This proposal consolidates how build strategies work internally and exposes that logic to third-party developers, creating an idiomatic way to extend Workbox with the goal of engendering a more active third-party ecosystem.
Background
Today, the primary way developers can customize or extend Workbox is to use plugins. Plugins work well for slight modifications to the behavior of existing strategies, but they don't work well in cases where a callback need to coordinate with other callbacks, and they don't work at all if the developer is writing their own custom route handler.
Furthermore, plugins today do not capture the full response lifecycle (e.g. there's no way to know when all the work done by a strategy is complete).
To address these shortcomings, this proposal updates our strategy and plugin infrastructure to enable the following new capabilities:
Make it possible for 3rd-party developers to write their own strategies that also integrate with the existing plugin ecosystem.
Add new plugin callbacks that better capture the entire strategy lifecycle (e.g. from beginning to end, including work passed to event.waitUntil()).
Make it easier for developers to use plugins to observe strategy responses (e.g. to gather analytics on cache-hit rate, cache lookup speed, network download, etc.)
Make it possible for developers writing their own strategies to expose their own plugin lifecycle hooks (not sure how many users will want this, but it would be possible with the proposed changes).
Overview
The best way to ensure third-party developers have the power to extend Workbox in ways that fully meet their needs is to base our own strategies on top of the extensibility mechanisms we expose to third-party developers.
Specifically, this proposal introduces a new way for third-party developers to define their own Workbox strategies, and all of our built-in strategies will be rewritten on top of this mechanism.
New strategy base class
With this proposal, all Workbox strategy classes (both built-in strategies as well as custom, third-party strategies) must extend the new Strategy base class.
The Strategy base class is responsible for two primary things:
Invoking plugin lifecycle callbacks common to all strategy handlers (e.g. when they start, respond, and end).
Creating a "handler" instance, that can manage state for each individual request a strategy is handling.
A new "handler" class
We currently have internal modules call fetchWrapper and cacheWrapper, which (as their name implies) wrap the various fetch and cache APIs with hooks into their lifecycle. This is the mechanism that currently allows plugins to work, but it's not exposed to developers.
The new "handler" class (which this proposal calls StrategyHandler) will expose these methods so custom strategies can call fetch() or cacheMatch() and have any plugins that were added to the strategy instance automatically invoked.
This class would also make it possible for developers to add their own custom, lifecycle callbacks that might be specific to their strategies, and they would "just work" with the existing plugin interface.
New plugin lifecycle state
In Workbox today, plugins are stateless. That means if a request for /index.html triggers both the requestWillFetch() and cachedResponseWillBeUsed() callbacks, those two callbacks have no way of communicating with each other or even knowing that they were triggered by the same request.
In this proposal, all plugin callbacks will also be passed a new state object. This state object will be unique to this particular plugin object and this particular strategy invocation (i.e. the call to handle()).
This allows developers to write plugins where one callback can conditionally do something based on what another callback in the same plugin did (e.g. compute the time delta between running requestWillFetch() and fetchDidSucceed or fetchDidFail()).
New plugin lifecycle callbacks
In order to fully leverage the plugin lifecycle state (mentioned above), you need to know when the lifecycle of a given strategy invocation starts and finishes.
To address this need (and others), the following new plugin lifecycle callbacks will be added:
handlerWillStart: called before any handler logic starts running. This callback can be used to set the initial handler state (e.g. record the start time).
handlerWillRespond: called before the strategies handle() method returns a response. This callback can be used to modify that response before returning it to a route handler or other custom logic.
handlerDidRespond: called after the strategy's handle() method returns a response. This callback can be used to record any final response details, e.g. after changes made by other plugins.
handlerDidComplete: called after all extend lifetime promises added to the event from the invocation of this strategy have settled. This callback can be used to report on any data that needs to wait until the handler is done in order to calculate (e.g. cache hit status, cache latency, network latency).
Developers implementing their own custom strategies do not have to worry about invoking these callbacks themselves; that's all handled by a new Strategy base class.
API Design
This proposal would add two new classes to the API of the workbox-strategy package:
Strategy
StrategyHandler
Strategy class
An abstract base class that all other strategy classes must extend from:
abstractclassStrategy{cacheName: string;plugins: WorkboxPlugin[];fetchOptions?: RequestInit;matchOptions?: CacheQueryOptions;/** * Classes extending the `Strategy` based class should implement this method, * and leverage the [`handler`]{@link module:workbox-strategies.StrategyHandler} * arg to perform all fetching and cache logic, which will ensure all relevant * cache, cache options, fetch options and plugins are used (per the current * strategy instance). */protectedabstract_handle(request: Request,handler: StrategyHandler): Promise<Response>;/** * Creates a new instance of the strategy and sets all documented option * properties as public instance properties. * * Note: if a custom strategy class extends the base Strategy class and does * not need more than these properties, it does not need to define its own * constructor. */constructor(options: {cacheName?: string;plugins?: WorkboxPlugin[];fetchOptions?: RequestInit;matchOptions?: CacheQueryOptions;}={});/** * Perform a request strategy and returns a `Promise` that will resolve with * a `Response`, invoking all relevant plugin callbacks. * * When a strategy instance is registered with a Workbox * [route]{@link module:workbox-routing.Route}, this method is automatically * called when the route matches. * * Alternatively, this method can be used in a standalone `FetchEvent` * listener by passing it to `event.respondWith()`. */handle(options: FetchEvent|RouteHandlerCallbackOptions,): Promise<Response>;/** * Similar to [`handle()`]{@link module:workbox-strategies.Strategy~handle}, but * instead of just returning a `Promise` that resolves to a `Response` it * it will return an tuple of [response, done] promises, where the former * (`response`) is equivalent to what `handle()` returns, and the latter is a * Promise that will resolve once any promises that were added to * `event.waitUntil()` as part of performing the strategy have completed. * * You can await the `done` promise to ensure any extra work performed by * the strategy (usually caching responses) completes successfully. */handleAll(options: FetchEvent|RouteHandlerCallbackOptions): [Promise<Response>,Promise<void>];}
StrategyHandler class
A class created every time a Strategy instance instance calls handle() or handleAll() that wraps all fetch and cache actions around plugin callbacks and keeps track of when the strategy is "done" (i.e. all added event.waitUntil() promises have resolved).
classStrategyHandler{publicrequest!: Request;publicurl?: URL;publicevent?: ExtendableEvent;publicparams?: any;/** * Creates a new instance associated with the passed strategy and event * that's handling the request. * * The constructor also initializes the state that will be passed to each of * the plugins handling this request. */constructor(strategy: Strategy,options: {request: Request;url?: URL;params?: any;event?: ExtendableEvent;},);/** * Fetches a given request (and invokes any applicable plugin callback * methods) using the `fetchOptions` and `plugins` defined on the strategy * object. * * The following plugin lifecycle methods are invoked when using this method: * - `requestWillFetch()` * - `fetchDidSucceed()` * - `fetchDidFail()` */fetch(input: RequestInfo): Promise<Response>;/** * Matches a request from the cache (and invokes any applicable plugin * callback methods) using the `cacheName`, `matchOptions`, and `plugins` * defined on the strategy object. * * The following plugin lifecycle methods are invoked when using this method: * - cacheKeyWillByUsed() * - cachedResponseWillByUsed() */cacheMatch(key: RequestInfo): Promise<Response|undefined>;/** * Puts a request/response pair in the cache (and invokes any applicable * plugin callback methods) using the `cacheName` and `plugins` defined on * the strategy object. * * The following plugin lifecycle methods are invoked when using this method: * - cacheKeyWillByUsed() * - cacheWillUpdate() * - cacheDidUpdate() */cachePut(key: RequestInfo,response: Response): Promise<void>;/** * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on * the response generated by `this.fetch()`. * * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, * so you do not have to manually call `waitUntil()` on the event. * */fetchAndCachePut(input: RequestInfo): Promise<Response>;/** * Returns true if the strategy has at least one plugin with the given * callback. */hasCallback<CextendskeyofWorkboxPlugin>(name: C): boolean;/** * Runs all plugin callbacks matching the given name, in order, passing the * given param object (merged ith the current plugin state) as the only * argument. * * Note: since this method runs all plugins, it's not suitable for cases * where the return value of a callback needs to be applied prior to calling * the next callback. See `iterateCallbacks()` below for how to handle that * case. */runCallbacks<CextendskeyofNonNullable<WorkboxPlugin>>(name: C,param: Omit<WorkboxPluginCallbackParam[C],'state'>,): Promise<void>;/** * Accepts a callback and returns an iterable of matching plugin callbacks, * where each callback is wrapped with the current handler state (i.e. when * you call each callback, whatever object parameter you pass it will * be merged with the plugin's current state). */*iterateCallbacks<CextendskeyofWorkboxPlugin>(name: C,): Generator<NonNullable<WorkboxPlugin[C]>>;/** * Adds a promise to the extend lifetime promises of the event event * associated with the request being handled (usually a `FetchEvent`). * * Note: you can await `doneWaiting()` to know when all added promises have * settled. */waitUntil(promise: Promise<any>): Promise<any>;/** * Returns a promise that resolves once all promises passed to `waitUntil()` * have settled. * * Note: any work done after `doneWaiting()` settles should be manually * passed to an event's `waitUntil()` method (not this handler's * `waitUntil()` method), otherwise the service worker thread my be killed * prior to your work completing. */doneWaiting(): Promise<void>;/** * Stops running the strategy and immediately resolves any pending * `waitUntil()` promises. */destroy(): void}
Example usage
Perhaps the easiest way to understand this proposal is to take a look at how a developer wanting to build a custom strategy will do so. (Note: built-in strategies will use this same mechanism)
To define your own strategy class, all you have to do is:
Import the Strategy base class form the workbox-strategies package,
Define a new, custom class that extends the Strategy base class.
Implement the _handle() method with your own handling logic.
A simplest example:
This example shows how we'd re-implement our NetworkOnly strategy if this proposal were implemented (logging code omitted):
By not defining a constructor function, this strategy inherits the base Strategy constructor, which accepts a {cacheName, plugins, fetchOptions, matchOptions} param and sets those values on the instance.
The _handle() is invoked with two parameters
request, which is the Request the strategy is going to return a response for
handler a StrategyHandler instance, automatically created for the current strategy.
Note, in this example, rather than calling the native fetch() method, it's calling handler.fetch(), which automatically call all plugin lifecycle callbacks associated with fetching a resource.
A more complex example:
This example shows how a third-party developer could define their own strategy. This example is based on cache-network-race from the Offline Cookbook (which Workbox does not provide), but goes a step further and always updates the cache after a successful network request.
import{Strategy}from'workbox-strategies';classCacheNetworkRaceextendsStrategy{_handle(request,handler){constfetchAndCachePutDone=handler.fetchAndCachePut(request);constcacheMatchDone=handler.cacheMatch(request);returnnewPromise((resolve,reject)=>{fetchAndCachePutDone.then(resolve);cacheMatchDone.then((response)=>response&&resolve(response));// Reject if both network and cache error or find no response.Promise.allSettled([fetchAndCachePutDone,cacheMatchDone]).then((results)=>{const[fetchAndCachePutResult,cacheMatchResult]=results;if(fetchAndCachePutResult.status==='rejected'&&!cacheMatchResult.value){reject(fetchAndCachePutResult.reason);}});});}}
An example plugin:
The example shows how you can use the new state property of each plugin callback param to coordinate between plugins across the entire response lifecycle.
The plugin below can be used with strategies that provide a cache-first response but also hit the network (e.g. CacheFirst and StaleWhileRevalidate). It reports on whether or not there was a cache hit, and if not it calculates how long the fetch took.
A plugin like this could be used to track the performance of various service worker response strategies and send that data to an analytics tool.
constcacheFirstPerformanceReportPlugin={requestWillFetch: async({request, state})=>{// Store the start time of the request (if run).state.fetchStartTime=performance.now();returnrequest;},fetchDidSucceed: async({response, state})=>{// Store the end time of the request (if run).state.fetchEndTime=performance.now();returnresponse;},cachedResponseWillBeUsed: async({cachedResponse, state})=>{if(cachedResponse){// Store the cache hit status.state.cacheHit=true;}},handlerDidComplete: async({request, state})=>{if(state.cacheHit){console.log(request.url,`Response was fulfilled from the cache`);}else{constfetchTime=state.fetchEndTime-state.fetchStartTime;console.log(request.url,`Response not in the cache, fetch took ${fetchTime} ms`);}},};
The text was updated successfully, but these errors were encountered:
Objective
Workbox comes with a set of plugins and strategies that are very easy to use (and use together), but if a developer wants a slightly different behavior from what we provide out of the box, they often have to start from scratch.
This proposal consolidates how build strategies work internally and exposes that logic to third-party developers, creating an idiomatic way to extend Workbox with the goal of engendering a more active third-party ecosystem.
Background
Today, the primary way developers can customize or extend Workbox is to use plugins. Plugins work well for slight modifications to the behavior of existing strategies, but they don't work well in cases where a callback need to coordinate with other callbacks, and they don't work at all if the developer is writing their own custom route handler.
Furthermore, plugins today do not capture the full response lifecycle (e.g. there's no way to know when all the work done by a strategy is complete).
To address these shortcomings, this proposal updates our strategy and plugin infrastructure to enable the following new capabilities:
event.waitUntil()
).Overview
The best way to ensure third-party developers have the power to extend Workbox in ways that fully meet their needs is to base our own strategies on top of the extensibility mechanisms we expose to third-party developers.
Specifically, this proposal introduces a new way for third-party developers to define their own Workbox strategies, and all of our built-in strategies will be rewritten on top of this mechanism.
New strategy base class
With this proposal, all Workbox strategy classes (both built-in strategies as well as custom, third-party strategies) must extend the new
Strategy
base class.The
Strategy
base class is responsible for two primary things:A new "handler" class
We currently have internal modules call
fetchWrapper
andcacheWrapper
, which (as their name implies) wrap the various fetch and cache APIs with hooks into their lifecycle. This is the mechanism that currently allows plugins to work, but it's not exposed to developers.The new "handler" class (which this proposal calls
StrategyHandler
) will expose these methods so custom strategies can callfetch()
orcacheMatch()
and have any plugins that were added to the strategy instance automatically invoked.This class would also make it possible for developers to add their own custom, lifecycle callbacks that might be specific to their strategies, and they would "just work" with the existing plugin interface.
New plugin lifecycle state
In Workbox today, plugins are stateless. That means if a request for
/index.html
triggers both therequestWillFetch()
andcachedResponseWillBeUsed()
callbacks, those two callbacks have no way of communicating with each other or even knowing that they were triggered by the same request.In this proposal, all plugin callbacks will also be passed a new
state
object. This state object will be unique to this particular plugin object and this particular strategy invocation (i.e. the call tohandle()
).This allows developers to write plugins where one callback can conditionally do something based on what another callback in the same plugin did (e.g. compute the time delta between running
requestWillFetch()
andfetchDidSucceed
orfetchDidFail()
).New plugin lifecycle callbacks
In order to fully leverage the plugin lifecycle state (mentioned above), you need to know when the lifecycle of a given strategy invocation starts and finishes.
To address this need (and others), the following new plugin lifecycle callbacks will be added:
handle()
method returns a response. This callback can be used to modify that response before returning it to a route handler or other custom logic.handle()
method returns a response. This callback can be used to record any final response details, e.g. after changes made by other plugins.Developers implementing their own custom strategies do not have to worry about invoking these callbacks themselves; that's all handled by a new
Strategy
base class.API Design
This proposal would add two new classes to the API of the
workbox-strategy
package:Strategy
StrategyHandler
Strategy
classAn abstract base class that all other strategy classes must extend from:
StrategyHandler
classA class created every time a Strategy instance instance calls
handle()
orhandleAll()
that wraps all fetch and cache actions around plugin callbacks and keeps track of when the strategy is "done" (i.e. all addedevent.waitUntil()
promises have resolved).Example usage
Perhaps the easiest way to understand this proposal is to take a look at how a developer wanting to build a custom strategy will do so. (Note: built-in strategies will use this same mechanism)
To define your own strategy class, all you have to do is:
Strategy
base class form theworkbox-strategies
package,Strategy
base class._handle()
method with your own handling logic.A simplest example:
This example shows how we'd re-implement our
NetworkOnly
strategy if this proposal were implemented (logging code omitted):By not defining a constructor function, this strategy inherits the base
Strategy
constructor, which accepts a{cacheName, plugins, fetchOptions, matchOptions}
param and sets those values on the instance.The
_handle()
is invoked with two parametersrequest
, which is theRequest
the strategy is going to return a response forhandler
aStrategyHandler
instance, automatically created for the current strategy.Note, in this example, rather than calling the native
fetch()
method, it's callinghandler.fetch()
, which automatically call all plugin lifecycle callbacks associated with fetching a resource.A more complex example:
This example shows how a third-party developer could define their own strategy. This example is based on cache-network-race from the Offline Cookbook (which Workbox does not provide), but goes a step further and always updates the cache after a successful network request.
An example plugin:
The example shows how you can use the new
state
property of each plugin callback param to coordinate between plugins across the entire response lifecycle.The plugin below can be used with strategies that provide a cache-first response but also hit the network (e.g.
CacheFirst
andStaleWhileRevalidate
). It reports on whether or not there was a cache hit, and if not it calculates how long the fetch took.A plugin like this could be used to track the performance of various service worker response strategies and send that data to an analytics tool.
The text was updated successfully, but these errors were encountered: