Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@serwist/strategies

Package Overview
Dependencies
Maintainers
1
Versions
52
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@serwist/strategies - npm Package Compare versions

Comparing version 9.0.0-preview.17 to 9.0.0-preview.18

23

dist/index.d.ts

@@ -1,22 +0,3 @@

import { CacheFirst } from "./CacheFirst.js";
import { CacheOnly } from "./CacheOnly.js";
import type { NetworkFirstOptions } from "./NetworkFirst.js";
import { NetworkFirst } from "./NetworkFirst.js";
import type { NetworkOnlyOptions } from "./NetworkOnly.js";
import { NetworkOnly } from "./NetworkOnly.js";
import { StaleWhileRevalidate } from "./StaleWhileRevalidate.js";
import type { StrategyOptions } from "./Strategy.js";
import { Strategy } from "./Strategy.js";
import { StrategyHandler } from "./StrategyHandler.js";
declare global {
interface FetchEvent {
readonly preloadResponse: Promise<any>;
}
}
/**
* There are common caching strategies that most service workers will need
* and use. This module provides simple implementations of these strategies.
*/
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler };
export type { NetworkFirstOptions, NetworkOnlyOptions, StrategyOptions };
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler } from "@serwist/sw/strategies";
export type { NetworkFirstOptions, NetworkOnlyOptions, StrategyOptions } from "@serwist/sw/strategies";
//# sourceMappingURL=index.d.ts.map

@@ -1,738 +0,1 @@

import { assert, Deferred, logger, getFriendlyURL, SerwistError, timeout, cacheMatchIgnoreParams, executeQuotaErrorCallbacks, privateCacheNames } from '@serwist/core/internal';
function toRequest(input) {
return typeof input === "string" ? new Request(input) : input;
}
class StrategyHandler {
event;
request;
url;
params;
_cacheKeys = {};
_strategy;
_handlerDeferred;
_extendLifetimePromises;
_plugins;
_pluginStateMap;
constructor(strategy, options){
if (process.env.NODE_ENV !== "production") {
assert.isInstance(options.event, ExtendableEvent, {
moduleName: "@serwist/strategies",
className: "StrategyHandler",
funcName: "constructor",
paramName: "options.event"
});
assert.isInstance(options.request, Request, {
moduleName: "@serwist/strategies",
className: "StrategyHandler",
funcName: "constructor",
paramName: "options.request"
});
}
this.event = options.event;
this.request = options.request;
if (options.url) {
this.url = options.url;
this.params = options.params;
}
this._strategy = strategy;
this._handlerDeferred = new Deferred();
this._extendLifetimePromises = [];
this._plugins = [
...strategy.plugins
];
this._pluginStateMap = new Map();
for (const plugin of this._plugins){
this._pluginStateMap.set(plugin, {});
}
this.event.waitUntil(this._handlerDeferred.promise);
}
async fetch(input) {
const { event } = this;
let request = toRequest(input);
if (request.mode === "navigate" && event instanceof FetchEvent && event.preloadResponse) {
const possiblePreloadResponse = await event.preloadResponse;
if (possiblePreloadResponse) {
if (process.env.NODE_ENV !== "production") {
logger.log(`Using a preloaded navigation response for '${getFriendlyURL(request.url)}'`);
}
return possiblePreloadResponse;
}
}
const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
try {
for (const cb of this.iterateCallbacks("requestWillFetch")){
request = await cb({
request: request.clone(),
event
});
}
} catch (err) {
if (err instanceof Error) {
throw new SerwistError("plugin-error-request-will-fetch", {
thrownErrorMessage: err.message
});
}
}
const pluginFilteredRequest = request.clone();
try {
let fetchResponse;
fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
if (process.env.NODE_ENV !== "production") {
logger.debug(`Network request for '${getFriendlyURL(request.url)}' returned a response with status '${fetchResponse.status}'.`);
}
for (const callback of this.iterateCallbacks("fetchDidSucceed")){
fetchResponse = await callback({
event,
request: pluginFilteredRequest,
response: fetchResponse
});
}
return fetchResponse;
} catch (error) {
if (process.env.NODE_ENV !== "production") {
logger.log(`Network request for '${getFriendlyURL(request.url)}' threw an error.`, error);
}
if (originalRequest) {
await this.runCallbacks("fetchDidFail", {
error: error,
event,
originalRequest: originalRequest.clone(),
request: pluginFilteredRequest.clone()
});
}
throw error;
}
}
async fetchAndCachePut(input) {
const response = await this.fetch(input);
const responseClone = response.clone();
void this.waitUntil(this.cachePut(input, responseClone));
return response;
}
async cacheMatch(key) {
const request = toRequest(key);
let cachedResponse;
const { cacheName, matchOptions } = this._strategy;
const effectiveRequest = await this.getCacheKey(request, "read");
const multiMatchOptions = {
...matchOptions,
...{
cacheName
}
};
cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
if (process.env.NODE_ENV !== "production") {
if (cachedResponse) {
logger.debug(`Found a cached response in '${cacheName}'.`);
} else {
logger.debug(`No cached response found in '${cacheName}'.`);
}
}
for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")){
cachedResponse = await callback({
cacheName,
matchOptions,
cachedResponse,
request: effectiveRequest,
event: this.event
}) || undefined;
}
return cachedResponse;
}
async cachePut(key, response) {
const request = toRequest(key);
await timeout(0);
const effectiveRequest = await this.getCacheKey(request, "write");
if (process.env.NODE_ENV !== "production") {
if (effectiveRequest.method && effectiveRequest.method !== "GET") {
throw new SerwistError("attempt-to-cache-non-get-request", {
url: getFriendlyURL(effectiveRequest.url),
method: effectiveRequest.method
});
}
}
if (!response) {
if (process.env.NODE_ENV !== "production") {
logger.error(`Cannot cache non-existent response for '${getFriendlyURL(effectiveRequest.url)}'.`);
}
throw new SerwistError("cache-put-with-no-response", {
url: getFriendlyURL(effectiveRequest.url)
});
}
const responseToCache = await this._ensureResponseSafeToCache(response);
if (!responseToCache) {
if (process.env.NODE_ENV !== "production") {
logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' will not be cached.`, responseToCache);
}
return false;
}
const { cacheName, matchOptions } = this._strategy;
const cache = await self.caches.open(cacheName);
if (process.env.NODE_ENV !== "production") {
const vary = response.headers.get("Vary");
if (vary && matchOptions?.ignoreVary !== true) {
logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} has a 'Vary: ${vary}' header. Consider setting the {ignoreVary: true} option on your strategy to ensure cache matching and deletion works as expected.`);
}
}
const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(cache, effectiveRequest.clone(), [
"__WB_REVISION__"
], matchOptions) : null;
if (process.env.NODE_ENV !== "production") {
logger.debug(`Updating the '${cacheName}' cache with a new Response for ${getFriendlyURL(effectiveRequest.url)}.`);
}
try {
await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
} catch (error) {
if (error instanceof Error) {
if (error.name === "QuotaExceededError") {
await executeQuotaErrorCallbacks();
}
throw error;
}
}
for (const callback of this.iterateCallbacks("cacheDidUpdate")){
await callback({
cacheName,
oldResponse,
newResponse: responseToCache.clone(),
request: effectiveRequest,
event: this.event
});
}
return true;
}
async getCacheKey(request, mode) {
const key = `${request.url} | ${mode}`;
if (!this._cacheKeys[key]) {
let effectiveRequest = request;
for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")){
effectiveRequest = toRequest(await callback({
mode,
request: effectiveRequest,
event: this.event,
params: this.params
}));
}
this._cacheKeys[key] = effectiveRequest;
}
return this._cacheKeys[key];
}
hasCallback(name) {
for (const plugin of this._strategy.plugins){
if (name in plugin) {
return true;
}
}
return false;
}
async runCallbacks(name, param) {
for (const callback of this.iterateCallbacks(name)){
await callback(param);
}
}
*iterateCallbacks(name) {
for (const plugin of this._strategy.plugins){
if (typeof plugin[name] === "function") {
const state = this._pluginStateMap.get(plugin);
const statefulCallback = (param)=>{
const statefulParam = {
...param,
state
};
return plugin[name](statefulParam);
};
yield statefulCallback;
}
}
}
waitUntil(promise) {
this._extendLifetimePromises.push(promise);
return promise;
}
async doneWaiting() {
let promise = undefined;
while(promise = this._extendLifetimePromises.shift()){
await promise;
}
}
destroy() {
this._handlerDeferred.resolve(null);
}
async _ensureResponseSafeToCache(response) {
let responseToCache = response;
let pluginsUsed = false;
for (const callback of this.iterateCallbacks("cacheWillUpdate")){
responseToCache = await callback({
request: this.request,
response: responseToCache,
event: this.event
}) || undefined;
pluginsUsed = true;
if (!responseToCache) {
break;
}
}
if (!pluginsUsed) {
if (responseToCache && responseToCache.status !== 200) {
responseToCache = undefined;
}
if (process.env.NODE_ENV !== "production") {
if (responseToCache) {
if (responseToCache.status !== 200) {
if (responseToCache.status === 0) {
logger.warn(`The response for '${this.request.url}' is an opaque response. The caching strategy that you're using will not cache opaque responses by default.`);
} else {
logger.debug(`The response for '${this.request.url}' returned a status code of '${response.status}' and won't be cached as a result.`);
}
}
}
}
}
return responseToCache;
}
}
class Strategy {
cacheName;
plugins;
fetchOptions;
matchOptions;
constructor(options = {}){
this.cacheName = privateCacheNames.getRuntimeName(options.cacheName);
this.plugins = options.plugins || [];
this.fetchOptions = options.fetchOptions;
this.matchOptions = options.matchOptions;
}
handle(options) {
const [responseDone] = this.handleAll(options);
return responseDone;
}
handleAll(options) {
if (options instanceof FetchEvent) {
options = {
event: options,
request: options.request
};
}
const event = options.event;
const request = typeof options.request === "string" ? new Request(options.request) : options.request;
const handler = new StrategyHandler(this, options.url ? {
event,
request,
url: options.url,
params: options.params
} : {
event,
request
});
const responseDone = this._getResponse(handler, request, event);
const handlerDone = this._awaitComplete(responseDone, handler, request, event);
return [
responseDone,
handlerDone
];
}
async _getResponse(handler, request, event) {
await handler.runCallbacks("handlerWillStart", {
event,
request
});
let response = undefined;
try {
response = await this._handle(request, handler);
if (response === undefined || response.type === "error") {
throw new SerwistError("no-response", {
url: request.url
});
}
} catch (error) {
if (error instanceof Error) {
for (const callback of handler.iterateCallbacks("handlerDidError")){
response = await callback({
error,
event,
request
});
if (response !== undefined) {
break;
}
}
}
if (!response) {
throw error;
}
if (process.env.NODE_ENV !== "production") {
throw logger.log(`While responding to '${getFriendlyURL(request.url)}', an ${error instanceof Error ? error.toString() : ""} error occurred. Using a fallback response provided by a handlerDidError plugin.`);
}
}
for (const callback of handler.iterateCallbacks("handlerWillRespond")){
response = await callback({
event,
request,
response
});
}
return response;
}
async _awaitComplete(responseDone, handler, request, event) {
let response = undefined;
let error = undefined;
try {
response = await responseDone;
} catch (error) {}
try {
await handler.runCallbacks("handlerDidRespond", {
event,
request,
response
});
await handler.doneWaiting();
} catch (waitUntilError) {
if (waitUntilError instanceof Error) {
error = waitUntilError;
}
}
await handler.runCallbacks("handlerDidComplete", {
event,
request,
response,
error
});
handler.destroy();
if (error) {
throw error;
}
}
}
const messages = {
strategyStart: (strategyName, request)=>`Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`,
printFinalResponse: (response)=>{
if (response) {
logger.groupCollapsed("View the final response here.");
logger.log(response || "[No response returned]");
logger.groupEnd();
}
}
};
class CacheFirst extends Strategy {
async _handle(request, handler) {
const logs = [];
if (process.env.NODE_ENV !== "production") {
assert.isInstance(request, Request, {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "makeRequest",
paramName: "request"
});
}
let response = await handler.cacheMatch(request);
let error = undefined;
if (!response) {
if (process.env.NODE_ENV !== "production") {
logs.push(`No response found in the '${this.cacheName}' cache. Will respond with a network request.`);
}
try {
response = await handler.fetchAndCachePut(request);
} catch (err) {
if (err instanceof Error) {
error = err;
}
}
if (process.env.NODE_ENV !== "production") {
if (response) {
logs.push("Got response from network.");
} else {
logs.push("Unable to get a response from the network.");
}
}
} else {
if (process.env.NODE_ENV !== "production") {
logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
}
}
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
for (const log of logs){
logger.log(log);
}
messages.printFinalResponse(response);
logger.groupEnd();
}
if (!response) {
throw new SerwistError("no-response", {
url: request.url,
error
});
}
return response;
}
}
class CacheOnly extends Strategy {
async _handle(request, handler) {
if (process.env.NODE_ENV !== "production") {
assert.isInstance(request, Request, {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "makeRequest",
paramName: "request"
});
}
const response = await handler.cacheMatch(request);
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
if (response) {
logger.log(`Found a cached response in the '${this.cacheName}' cache.`);
messages.printFinalResponse(response);
} else {
logger.log(`No response found in the '${this.cacheName}' cache.`);
}
logger.groupEnd();
}
if (!response) {
throw new SerwistError("no-response", {
url: request.url
});
}
return response;
}
}
const cacheOkAndOpaquePlugin = {
cacheWillUpdate: async ({ response })=>{
if (response.status === 200 || response.status === 0) {
return response;
}
return null;
}
};
class NetworkFirst extends Strategy {
_networkTimeoutSeconds;
constructor(options = {}){
super(options);
if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
this.plugins.unshift(cacheOkAndOpaquePlugin);
}
this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
if (process.env.NODE_ENV !== "production") {
if (this._networkTimeoutSeconds) {
assert.isType(this._networkTimeoutSeconds, "number", {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "constructor",
paramName: "networkTimeoutSeconds"
});
}
}
}
async _handle(request, handler) {
const logs = [];
if (process.env.NODE_ENV !== "production") {
assert.isInstance(request, Request, {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "handle",
paramName: "makeRequest"
});
}
const promises = [];
let timeoutId;
if (this._networkTimeoutSeconds) {
const { id, promise } = this._getTimeoutPromise({
request,
logs,
handler
});
timeoutId = id;
promises.push(promise);
}
const networkPromise = this._getNetworkPromise({
timeoutId,
request,
logs,
handler
});
promises.push(networkPromise);
const response = await handler.waitUntil((async ()=>{
return await handler.waitUntil(Promise.race(promises)) || await networkPromise;
})());
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
for (const log of logs){
logger.log(log);
}
messages.printFinalResponse(response);
logger.groupEnd();
}
if (!response) {
throw new SerwistError("no-response", {
url: request.url
});
}
return response;
}
_getTimeoutPromise({ request, logs, handler }) {
let timeoutId;
const timeoutPromise = new Promise((resolve)=>{
const onNetworkTimeout = async ()=>{
if (process.env.NODE_ENV !== "production") {
logs.push(`Timing out the network response at ${this._networkTimeoutSeconds} seconds.`);
}
resolve(await handler.cacheMatch(request));
};
timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000);
});
return {
promise: timeoutPromise,
id: timeoutId
};
}
async _getNetworkPromise({ timeoutId, request, logs, handler }) {
let error = undefined;
let response = undefined;
try {
response = await handler.fetchAndCachePut(request);
} catch (fetchError) {
if (fetchError instanceof Error) {
error = fetchError;
}
}
if (timeoutId) {
clearTimeout(timeoutId);
}
if (process.env.NODE_ENV !== "production") {
if (response) {
logs.push("Got response from network.");
} else {
logs.push("Unable to get a response from the network. Will respond " + "with a cached response.");
}
}
if (error || !response) {
response = await handler.cacheMatch(request);
if (process.env.NODE_ENV !== "production") {
if (response) {
logs.push(`Found a cached response in the '${this.cacheName}' cache.`);
} else {
logs.push(`No response found in the '${this.cacheName}' cache.`);
}
}
}
return response;
}
}
class NetworkOnly extends Strategy {
_networkTimeoutSeconds;
constructor(options = {}){
super(options);
this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0;
}
async _handle(request, handler) {
if (process.env.NODE_ENV !== "production") {
assert.isInstance(request, Request, {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "_handle",
paramName: "request"
});
}
let error = undefined;
let response;
try {
const promises = [
handler.fetch(request)
];
if (this._networkTimeoutSeconds) {
const timeoutPromise = timeout(this._networkTimeoutSeconds * 1000);
promises.push(timeoutPromise);
}
response = await Promise.race(promises);
if (!response) {
throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`);
}
} catch (err) {
if (err instanceof Error) {
error = err;
}
}
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
if (response) {
logger.log("Got response from network.");
} else {
logger.log("Unable to get a response from the network.");
}
messages.printFinalResponse(response);
logger.groupEnd();
}
if (!response) {
throw new SerwistError("no-response", {
url: request.url,
error
});
}
return response;
}
}
class StaleWhileRevalidate extends Strategy {
constructor(options = {}){
super(options);
if (!this.plugins.some((p)=>"cacheWillUpdate" in p)) {
this.plugins.unshift(cacheOkAndOpaquePlugin);
}
}
async _handle(request, handler) {
const logs = [];
if (process.env.NODE_ENV !== "production") {
assert.isInstance(request, Request, {
moduleName: "@serwist/strategies",
className: this.constructor.name,
funcName: "handle",
paramName: "request"
});
}
const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(()=>{});
void handler.waitUntil(fetchAndCachePromise);
let response = await handler.cacheMatch(request);
let error = undefined;
if (response) {
if (process.env.NODE_ENV !== "production") {
logs.push(`Found a cached response in the '${this.cacheName}' cache. Will update with the network response in the background.`);
}
} else {
if (process.env.NODE_ENV !== "production") {
logs.push(`No response found in the '${this.cacheName}' cache. Will wait for the network response.`);
}
try {
response = await fetchAndCachePromise;
} catch (err) {
if (err instanceof Error) {
error = err;
}
}
}
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(messages.strategyStart(this.constructor.name, request));
for (const log of logs){
logger.log(log);
}
messages.printFinalResponse(response);
logger.groupEnd();
}
if (!response) {
throw new SerwistError("no-response", {
url: request.url,
error
});
}
return response;
}
}
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler };
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler } from '@serwist/sw/strategies';

6

package.json
{
"name": "@serwist/strategies",
"version": "9.0.0-preview.17",
"version": "9.0.0-preview.18",
"type": "module",

@@ -33,3 +33,3 @@ "description": "A service worker helper library implementing common caching strategies.",

"dependencies": {
"@serwist/core": "9.0.0-preview.17"
"@serwist/sw": "9.0.0-preview.18"
},

@@ -39,3 +39,3 @@ "devDependencies": {

"typescript": "5.5.0-dev.20240323",
"@serwist/constants": "9.0.0-preview.17"
"@serwist/constants": "9.0.0-preview.18"
},

@@ -42,0 +42,0 @@ "peerDependencies": {

@@ -1,34 +0,2 @@

/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import { CacheFirst } from "./CacheFirst.js";
import { CacheOnly } from "./CacheOnly.js";
import type { NetworkFirstOptions } from "./NetworkFirst.js";
import { NetworkFirst } from "./NetworkFirst.js";
import type { NetworkOnlyOptions } from "./NetworkOnly.js";
import { NetworkOnly } from "./NetworkOnly.js";
import { StaleWhileRevalidate } from "./StaleWhileRevalidate.js";
import type { StrategyOptions } from "./Strategy.js";
import { Strategy } from "./Strategy.js";
import { StrategyHandler } from "./StrategyHandler.js";
// See https://github.com/GoogleChrome/workbox/issues/2946
declare global {
interface FetchEvent {
// See https://github.com/GoogleChrome/workbox/issues/2974
readonly preloadResponse: Promise<any>;
}
}
/**
* There are common caching strategies that most service workers will need
* and use. This module provides simple implementations of these strategies.
*/
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler };
export type { NetworkFirstOptions, NetworkOnlyOptions, StrategyOptions };
export { CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate, Strategy, StrategyHandler } from "@serwist/sw/strategies";
export type { NetworkFirstOptions, NetworkOnlyOptions, StrategyOptions } from "@serwist/sw/strategies";

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc