@ohos-ports/layer0-prefetch 4.20.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +7 -0
  2. package/constants.d.ts +23 -0
  3. package/constants.js +25 -0
  4. package/index.js +24 -0
  5. package/package.json +50 -0
  6. package/sw/CustomCacheKeyPlugin.d.ts +10 -0
  7. package/sw/CustomCacheKeyPlugin.js +54 -0
  8. package/sw/CustomExpirationPlugin.d.ts +122 -0
  9. package/sw/CustomExpirationPlugin.js +280 -0
  10. package/sw/DeepFetchPlugin.d.ts +108 -0
  11. package/sw/DeepFetchPlugin.js +144 -0
  12. package/sw/DevtoolsPlugin.d.ts +64 -0
  13. package/sw/DevtoolsPlugin.js +131 -0
  14. package/sw/PrefetchPlugin.d.ts +32 -0
  15. package/sw/PrefetchPlugin.js +167 -0
  16. package/sw/Prefetcher.d.ts +209 -0
  17. package/sw/Prefetcher.js +367 -0
  18. package/sw/getServiceWorkerMaxAge.d.ts +22 -0
  19. package/sw/getServiceWorkerMaxAge.js +51 -0
  20. package/sw/index.d.ts +2 -0
  21. package/sw/index.js +6 -0
  22. package/sw/log.d.ts +1 -0
  23. package/sw/log.js +9 -0
  24. package/sw/messageBrowser.d.ts +7 -0
  25. package/sw/messageBrowser.js +18 -0
  26. package/sw/prefetch.d.ts +9 -0
  27. package/sw/prefetch.js +17 -0
  28. package/window/InstallOptions.d.ts +77 -0
  29. package/window/InstallOptions.js +2 -0
  30. package/window/PrefetchOptions.d.ts +19 -0
  31. package/window/PrefetchOptions.js +2 -0
  32. package/window/entry.d.ts +1 -0
  33. package/window/entry.js +8 -0
  34. package/window/getHostName.d.ts +1 -0
  35. package/window/getHostName.js +6 -0
  36. package/window/getUserAgent.d.ts +1 -0
  37. package/window/getUserAgent.js +6 -0
  38. package/window/index.d.ts +3 -0
  39. package/window/index.js +8 -0
  40. package/window/install.d.ts +13 -0
  41. package/window/install.js +72 -0
  42. package/window/isSafari.d.ts +6 -0
  43. package/window/isSafari.js +31 -0
  44. package/window/loadManifest.d.ts +3 -0
  45. package/window/loadManifest.js +20 -0
  46. package/window/prefetch.d.ts +71 -0
  47. package/window/prefetch.js +157 -0
  48. package/window/prefetchWhenVisible.d.ts +5 -0
  49. package/window/prefetchWhenVisible.js +20 -0
  50. package/window/registerServiceWorker.d.ts +2 -0
  51. package/window/registerServiceWorker.js +66 -0
  52. package/window/runWhenVisible.d.ts +6 -0
  53. package/window/runWhenVisible.js +28 -0
  54. package/window/waitForServiceWorker.d.ts +4 -0
  55. package/window/waitForServiceWorker.js +27 -0
  56. package/window/watchElements.d.ts +7 -0
  57. package/window/watchElements.js +63 -0
  58. package/window/watchLinks.d.ts +9 -0
  59. package/window/watchLinks.js +65 -0
@@ -0,0 +1,108 @@
1
+ /// <reference types="cheerio" />
2
+ import { WorkboxPlugin, FetchDidSucceedCallbackParam } from 'workbox-core/types';
3
+ import { Options } from 'json-query';
4
+ export interface DeepFetchCallbackParam {
5
+ /**
6
+ * The matching element, wrapped in a cheerio instance
7
+ */
8
+ $el: Cheerio;
9
+ /**
10
+ * The matching element
11
+ */
12
+ el: CheerioElement;
13
+ /**
14
+ * The cheerio static instance
15
+ */
16
+ $: CheerioStatic;
17
+ /**
18
+ * The URL of the page that was prefetched
19
+ */
20
+ srcURL: string;
21
+ }
22
+ interface DeepFetchBaseConfig {
23
+ /**
24
+ * The max number of matching elements to prefetch per page
25
+ */
26
+ maxMatches?: number;
27
+ /**
28
+ * The value for the `<link rel="prefetch|preload">` element's `as` attribute.
29
+ * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link. Use "fetch" for html pages as Safari doesn't support "document".
30
+ */
31
+ as: 'image' | 'script' | 'style' | 'font' | 'fetch' | 'video';
32
+ /**
33
+ * The value of the `crossorigin` attribute of the prefetch link. Defaults to 'anonymous'. Setting this to `false`
34
+ * makes it so the `crossorigin` attribute is omitted, i.e. no CORS is used.
35
+ */
36
+ cors?: false | 'anonymous' | 'use-credentials';
37
+ }
38
+ interface DeepFetchHtmlConfig extends DeepFetchBaseConfig {
39
+ /**
40
+ * The CSS selector to use when finding elements whose src or href should be deep fetched
41
+ */
42
+ selector: string;
43
+ /**
44
+ * The attribute whose value should be prefetched. Defaults to `src`.
45
+ */
46
+ attribute: string;
47
+ /**
48
+ * A callback to override the default prefetching logic. Will be called for each matching element,
49
+ * or if `maxMatches` is provided, the first n elements.
50
+ */
51
+ callback?: (param: DeepFetchCallbackParam) => void;
52
+ }
53
+ interface DeepFetchJsonConfig extends DeepFetchBaseConfig {
54
+ /**
55
+ * A json query to use when finding data within a JSON response that should be deep
56
+ * fetched.
57
+ * Queries are made using the [json-query](https://github.com/auditassistant/json-query)
58
+ * library. Properties returned by the query are expected to be strings; any non-string
59
+ * results are ignored.
60
+ */
61
+ jsonQuery: string;
62
+ /**
63
+ * Options that can be passed to [json-query](https://github.com/auditassistant/json-query#options).
64
+ */
65
+ jsonQueryOptions?: Options;
66
+ }
67
+ declare type DeepFetchConfig = DeepFetchHtmlConfig | DeepFetchJsonConfig;
68
+ /**
69
+ * This plugin deep fetches resources during prefetching based on CSS selectors.
70
+ *
71
+ * ** Example **
72
+ *
73
+ * ```js
74
+ * import DeepFetchPlugin from '@layer0/prefetch/sw/DeepFetchPlugin'
75
+ * import { Prefetcher } from '@layer0/prefetch/sw'
76
+ *
77
+ * new Prefetcher({
78
+ * plugins: [
79
+ * new DeepFetchPlugin([
80
+ * {
81
+ * selector: 'img.product-main-image,
82
+ * maxMatches: 1,
83
+ * as: 'image',
84
+ * attribute: 'src'
85
+ * }
86
+ * ])
87
+ * ]
88
+ * })
89
+ * ```
90
+ */
91
+ export default class DeepFetchPlugin implements WorkboxPlugin {
92
+ config: DeepFetchConfig[];
93
+ constructor(config: DeepFetchConfig[]);
94
+ get htmlConfigs(): DeepFetchHtmlConfig[];
95
+ get jsonConfigs(): DeepFetchJsonConfig[];
96
+ /**
97
+ * WorkboxPlugin method called every time a fetch succeeds.
98
+ */
99
+ fetchDidSucceed({ request, response }: FetchDidSucceedCallbackParam): Promise<Response>;
100
+ /**
101
+ * Deep fetches elements that match the selectors provided by the user.
102
+ * @param html The document to search
103
+ * @param srcURL URL of the source document
104
+ */
105
+ private deepFetchHtml;
106
+ private deepFetchJson;
107
+ }
108
+ export {};
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const cheerio_1 = __importDefault(require("cheerio"));
7
+ const prefetch_1 = require("./prefetch");
8
+ const log_1 = __importDefault(require("./log"));
9
+ const json_query_1 = __importDefault(require("json-query"));
10
+ function isHtmlConfig(config) {
11
+ return config.selector !== undefined;
12
+ }
13
+ function isJsonConfig(config) {
14
+ return config.jsonQuery !== undefined;
15
+ }
16
+ /**
17
+ * This plugin deep fetches resources during prefetching based on CSS selectors.
18
+ *
19
+ * ** Example **
20
+ *
21
+ * ```js
22
+ * import DeepFetchPlugin from '@layer0/prefetch/sw/DeepFetchPlugin'
23
+ * import { Prefetcher } from '@layer0/prefetch/sw'
24
+ *
25
+ * new Prefetcher({
26
+ * plugins: [
27
+ * new DeepFetchPlugin([
28
+ * {
29
+ * selector: 'img.product-main-image,
30
+ * maxMatches: 1,
31
+ * as: 'image',
32
+ * attribute: 'src'
33
+ * }
34
+ * ])
35
+ * ]
36
+ * })
37
+ * ```
38
+ */
39
+ class DeepFetchPlugin {
40
+ constructor(config) {
41
+ this.config = config;
42
+ }
43
+ get htmlConfigs() {
44
+ return this.config.filter(isHtmlConfig);
45
+ }
46
+ get jsonConfigs() {
47
+ return this.config.filter(isJsonConfig);
48
+ }
49
+ /**
50
+ * WorkboxPlugin method called every time a fetch succeeds.
51
+ */
52
+ async fetchDidSucceed({ request, response }) {
53
+ try {
54
+ const contentType = response.headers.get('content-type');
55
+ if (contentType && contentType.toLowerCase().includes('text/html')) {
56
+ response
57
+ .clone()
58
+ .text()
59
+ .then(html => this.deepFetchHtml(html, request.url));
60
+ }
61
+ else if (contentType && contentType.toLowerCase().includes('application/json')) {
62
+ response
63
+ .clone()
64
+ .json()
65
+ .then(obj => this.deepFetchJson(obj, request.url));
66
+ }
67
+ }
68
+ finally {
69
+ // need to always return the response here, even if there is an error, or the browser will never get a response from the network
70
+ // eslint-disable-next-line no-unsafe-finally
71
+ return response;
72
+ }
73
+ }
74
+ /**
75
+ * Deep fetches elements that match the selectors provided by the user.
76
+ * @param html The document to search
77
+ * @param srcURL URL of the source document
78
+ */
79
+ async deepFetchHtml(html, srcURL) {
80
+ // cheerio >= 1.0.0 compiles its CJS build with __esModule = true and exports `load`
81
+ // as a named export, so `cheerio_1.default` may be undefined. Resolve `load` from
82
+ // either the default export or the module itself to support both module shapes
83
+ // (cheerio 1.0.0-rc.x CJS default export and cheerio >= 1.0.0 named exports).
84
+ const $ = (cheerio_1.default || cheerio_1).load(html);
85
+ for (let { selector, maxMatches, callback, as, attribute = 'src', cors = 'anonymous' } of this
86
+ .htmlConfigs) {
87
+ let matchCount = 0;
88
+ $(selector).each(function () {
89
+ if (maxMatches != null && matchCount >= maxMatches)
90
+ return;
91
+ if (this.tagName.toLowerCase() === 'a') {
92
+ log_1.default('warning', 'Deep fetching of html links is not supported.');
93
+ return;
94
+ }
95
+ const $el = $(this);
96
+ if (callback) {
97
+ matchCount++;
98
+ callback({ $el, el: this, $, srcURL });
99
+ }
100
+ else {
101
+ const url = $el.attr(attribute);
102
+ if (url) {
103
+ matchCount++;
104
+ prefetch_1.prefetch(url, as, { cors });
105
+ }
106
+ else {
107
+ log_1.default(`warning: DeepFetchPlugin found an element matching selector ${selector} which did not have a ${attribute} attribute.`);
108
+ }
109
+ }
110
+ });
111
+ }
112
+ }
113
+ async deepFetchJson(jsonObj, srcURL) {
114
+ for (let { jsonQuery: query, jsonQueryOptions, maxMatches, as, cors = 'anonymous' } of this
115
+ .jsonConfigs) {
116
+ let matchCount = 0;
117
+ const maybeDeepFetchJsonResult = (result) => {
118
+ if (maxMatches != null && matchCount >= maxMatches)
119
+ return;
120
+ if (typeof result === 'string') {
121
+ matchCount++;
122
+ prefetch_1.prefetch(result, as, { cors });
123
+ }
124
+ else {
125
+ console.warn(`Unable to deep fetch result: ${result} is not a valid URL string`);
126
+ return;
127
+ }
128
+ };
129
+ const { value: results } = json_query_1.default(query, {
130
+ data: jsonObj,
131
+ ...jsonQueryOptions,
132
+ });
133
+ if (Array.isArray(results)) {
134
+ results.forEach(result => {
135
+ maybeDeepFetchJsonResult(result);
136
+ });
137
+ }
138
+ else {
139
+ maybeDeepFetchJsonResult(results);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ exports.default = DeepFetchPlugin;
@@ -0,0 +1,64 @@
1
+ import { WorkboxPlugin, CachedResponseWillBeUsedCallbackParam, RequestWillFetchCallbackParam, FetchDidSucceedCallbackParam, CacheKeyWillBeUsedCallbackParam } from 'workbox-core/types';
2
+ import { RouteConfig } from './Prefetcher';
3
+ declare type CurrentRequestMetadata = {
4
+ isPrefetch?: boolean;
5
+ requestStart?: number;
6
+ };
7
+ export default class DevtoolsPlugin implements WorkboxPlugin {
8
+ private onResponse;
9
+ private isEnabledFn;
10
+ private routeConfig?;
11
+ private currentRequests;
12
+ constructor({ onResponse, routeConfig, isEnabledFn, }: {
13
+ onResponse: Function;
14
+ isEnabledFn: Function;
15
+ routeConfig?: RouteConfig;
16
+ });
17
+ /**
18
+ * Stores information about a request to be used by subsequent plugin steps
19
+ *
20
+ * IMPORTANT We do **not** use a custom header to store the metadata: a custom header leads
21
+ * to CORS pre-flight requests when using cache on a different origin, and those would likely
22
+ * reject our header.
23
+ * (more info https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests)
24
+ *
25
+ * Unfortunately, there does not seem to be another way to (or if there is, feel free to fix this):
26
+ * - store information on the request instance that we be passed along to subsequent steps
27
+ * - identify a request uniquely with some sort of internal id
28
+ *
29
+ * For now, the solution is to use the normalized cache key to identify each request "uniquely",
30
+ * although that would lead to inconsistent results if a same request happens multiple time concurrently.
31
+ * It also adds overhead, especially for POST graphql query which serializes the body.
32
+ *
33
+ * After CURRENT_REQUEST_MAP_CLEANUP_DELAY, the metadata is cleared to avoid a memory leak.
34
+ * We don't clear explicitly from cachedResponseWillBeUsed/fetchDidSucceed, as it very complex (impossible?) to do it right:
35
+ * - we would need a counter on the current requests to be sure to not clear the metadata from concurrent equivalent request
36
+ * - incrementing/decrementing because we don't know in advance through which
37
+ * plugin functions the request will go through, which is the first and which
38
+ * is the last. We could make assumptions but that would lead to fragility.
39
+ */
40
+ saveRequestMetadata(request: Request, metadata: CurrentRequestMetadata): Promise<void>;
41
+ getRequestMetadata(request: Request): Promise<CurrentRequestMetadata>;
42
+ /**
43
+ * Notes:
44
+ * - It seems that we cannot add arbitrary attributes to the request object
45
+ * so the best we can do is to store request info in a memory hash-map
46
+ */
47
+ requestWillFetch({ request }: RequestWillFetchCallbackParam): Promise<Request>;
48
+ /**
49
+ * Called when an object is read from or written to the cache.
50
+ *
51
+ * Notes:
52
+ * - It seems that we cannot add arbitrary attributes to the request object
53
+ * so the best we can do is to store request info in a memory hash-map
54
+ *
55
+ * - In case of cache 'read', we keep track of prefetch request so that we can identify them later
56
+ * in `cachedResponseWillBeUsed`, which is called for both hit or miss.
57
+ * Why here and not on response? Because in `cachedResponseWillBeUsed` we only get the cache-normalized request
58
+ * where layer0_dt_pf query string got removed
59
+ */
60
+ cacheKeyWillBeUsed({ request, mode }: CacheKeyWillBeUsedCallbackParam): Promise<Request>;
61
+ cachedResponseWillBeUsed({ request, cachedResponse: response, }: CachedResponseWillBeUsedCallbackParam): Promise<Response | undefined>;
62
+ fetchDidSucceed({ request, response }: FetchDidSucceedCallbackParam): Promise<Response>;
63
+ }
64
+ export {};
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const PrefetchPlugin_1 = require("./PrefetchPlugin");
4
+ /**
5
+ * Time after which we clear a given request metadata from memory.
6
+ * This must be above usual http timeouts to be sure the request is completed.
7
+ */
8
+ const CURRENT_REQUEST_MAP_CLEANUP_DELAY = 120000;
9
+ class DevtoolsPlugin {
10
+ constructor({ onResponse, routeConfig, isEnabledFn, }) {
11
+ this.currentRequests = {};
12
+ this.onResponse = onResponse;
13
+ this.routeConfig = routeConfig;
14
+ this.isEnabledFn = isEnabledFn;
15
+ }
16
+ /**
17
+ * Stores information about a request to be used by subsequent plugin steps
18
+ *
19
+ * IMPORTANT We do **not** use a custom header to store the metadata: a custom header leads
20
+ * to CORS pre-flight requests when using cache on a different origin, and those would likely
21
+ * reject our header.
22
+ * (more info https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests)
23
+ *
24
+ * Unfortunately, there does not seem to be another way to (or if there is, feel free to fix this):
25
+ * - store information on the request instance that we be passed along to subsequent steps
26
+ * - identify a request uniquely with some sort of internal id
27
+ *
28
+ * For now, the solution is to use the normalized cache key to identify each request "uniquely",
29
+ * although that would lead to inconsistent results if a same request happens multiple time concurrently.
30
+ * It also adds overhead, especially for POST graphql query which serializes the body.
31
+ *
32
+ * After CURRENT_REQUEST_MAP_CLEANUP_DELAY, the metadata is cleared to avoid a memory leak.
33
+ * We don't clear explicitly from cachedResponseWillBeUsed/fetchDidSucceed, as it very complex (impossible?) to do it right:
34
+ * - we would need a counter on the current requests to be sure to not clear the metadata from concurrent equivalent request
35
+ * - incrementing/decrementing because we don't know in advance through which
36
+ * plugin functions the request will go through, which is the first and which
37
+ * is the last. We could make assumptions but that would lead to fragility.
38
+ */
39
+ async saveRequestMetadata(request, metadata) {
40
+ var _a, _b;
41
+ const normalizedUrl = await PrefetchPlugin_1.normalizeRequestUrlForCache(request);
42
+ if ((_a = this.currentRequests[normalizedUrl]) === null || _a === void 0 ? void 0 : _a.cleanupTimeout) {
43
+ clearTimeout(this.currentRequests[normalizedUrl].cleanupTimeout);
44
+ }
45
+ this.currentRequests[normalizedUrl] = {
46
+ metadata: {
47
+ ...(_b = this.currentRequests[normalizedUrl]) === null || _b === void 0 ? void 0 : _b.metadata,
48
+ ...metadata,
49
+ },
50
+ cleanupTimeout: setTimeout(() => {
51
+ delete this.currentRequests[normalizedUrl];
52
+ }, CURRENT_REQUEST_MAP_CLEANUP_DELAY),
53
+ };
54
+ }
55
+ async getRequestMetadata(request) {
56
+ var _a;
57
+ const normalizedUrl = await PrefetchPlugin_1.normalizeRequestUrlForCache(request);
58
+ return ((_a = this.currentRequests[normalizedUrl]) === null || _a === void 0 ? void 0 : _a.metadata) || {};
59
+ }
60
+ /**
61
+ * Notes:
62
+ * - It seems that we cannot add arbitrary attributes to the request object
63
+ * so the best we can do is to store request info in a memory hash-map
64
+ */
65
+ async requestWillFetch({ request }) {
66
+ if (!this.isEnabledFn()) {
67
+ return request;
68
+ }
69
+ await this.saveRequestMetadata(request, { requestStart: Date.now() });
70
+ return request;
71
+ }
72
+ /**
73
+ * Called when an object is read from or written to the cache.
74
+ *
75
+ * Notes:
76
+ * - It seems that we cannot add arbitrary attributes to the request object
77
+ * so the best we can do is to store request info in a memory hash-map
78
+ *
79
+ * - In case of cache 'read', we keep track of prefetch request so that we can identify them later
80
+ * in `cachedResponseWillBeUsed`, which is called for both hit or miss.
81
+ * Why here and not on response? Because in `cachedResponseWillBeUsed` we only get the cache-normalized request
82
+ * where layer0_dt_pf query string got removed
83
+ */
84
+ async cacheKeyWillBeUsed({ request, mode }) {
85
+ if (!this.isEnabledFn()) {
86
+ return request;
87
+ }
88
+ if (mode === 'read') {
89
+ await this.saveRequestMetadata(request, {
90
+ isPrefetch: PrefetchPlugin_1.isPrefetchRequest(request),
91
+ });
92
+ }
93
+ return request;
94
+ }
95
+ async cachedResponseWillBeUsed({ request, cachedResponse: response, }) {
96
+ if (!this.isEnabledFn()) {
97
+ return response;
98
+ }
99
+ // In case of cache miss
100
+ if (!response)
101
+ return response;
102
+ const { isPrefetch } = await this.getRequestMetadata(request);
103
+ // TTFB is hard-coded to 0 for cached response
104
+ this.onResponse({
105
+ browserCache: 'hit',
106
+ ttfb: 0,
107
+ prefetch: isPrefetch,
108
+ request,
109
+ response,
110
+ routeConfig: this.routeConfig,
111
+ });
112
+ return response;
113
+ }
114
+ async fetchDidSucceed({ request, response }) {
115
+ if (!this.isEnabledFn()) {
116
+ return response;
117
+ }
118
+ const { requestStart } = await this.getRequestMetadata(request);
119
+ const ttfb = requestStart && Date.now() - requestStart;
120
+ this.onResponse({
121
+ browserCache: 'miss',
122
+ ttfb,
123
+ prefetch: PrefetchPlugin_1.isPrefetchRequest(request),
124
+ request,
125
+ response,
126
+ routeConfig: this.routeConfig,
127
+ });
128
+ return response;
129
+ }
130
+ }
131
+ exports.default = DevtoolsPlugin;
@@ -0,0 +1,32 @@
1
+ import { WorkboxPlugin, CacheKeyWillBeUsedCallbackParam, CacheWillUpdateCallbackParamParam, RequestWillFetchCallbackParam } from 'workbox-core/types';
2
+ export declare function isPrefetchRequest(request: Request): boolean;
3
+ export declare function normalizeRequestUrlForCache(request: Request, { cacheHost }?: {
4
+ cacheHost?: URL;
5
+ }): Promise<string>;
6
+ export default class PrefetchPlugin implements WorkboxPlugin {
7
+ private incrementRead;
8
+ private incrementWrite;
9
+ private cacheHost?;
10
+ private prefetchUpstreamRequests?;
11
+ private convertToGet?;
12
+ private method;
13
+ constructor({ incrementRead, incrementWrite, cacheHost, prefetchUpstreamRequests, convertToGet, method, }: {
14
+ incrementRead: Function;
15
+ incrementWrite: Function;
16
+ cacheHost?: URL;
17
+ prefetchUpstreamRequests?: boolean;
18
+ convertToGet?: boolean;
19
+ method: string;
20
+ });
21
+ requestWillFetch({ request }: RequestWillFetchCallbackParam): Promise<Request>;
22
+ /**
23
+ * Called when an object is read from or written to the cache. Here we
24
+ * compute cache stats and ensure that ?layer0_prefetch is removed from the cache key so
25
+ * that future requests for prefetched resources will match.
26
+ */
27
+ cacheKeyWillBeUsed({ request, mode }: CacheKeyWillBeUsedCallbackParam): Promise<string>;
28
+ /**
29
+ * Called when a response is about to be written to the cache.
30
+ */
31
+ cacheWillUpdate({ response, request, event }: CacheWillUpdateCallbackParamParam): Promise<Response | null>;
32
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const constants_1 = require("@layer0/core/constants");
7
+ const constants_2 = require("../constants");
8
+ const constants_3 = require("../constants");
9
+ const log_1 = __importDefault(require("./log"));
10
+ const prefetch_1 = require("./prefetch");
11
+ const getServiceWorkerMaxAge_1 = __importDefault(require("./getServiceWorkerMaxAge"));
12
+ const origin = new URL(self.origin);
13
+ function isPrefetchRequest(request) {
14
+ return new URL(request.url).searchParams.has(constants_1.DEVTOOLS_PREFETCH_QUERY_PARAM);
15
+ }
16
+ exports.isPrefetchRequest = isPrefetchRequest;
17
+ async function normalizeRequestUrlForCache(request, { cacheHost } = {}) {
18
+ request = request.clone();
19
+ const url = new URL(request.url);
20
+ // When reading/writing cacheHost URLs convert them back to origin
21
+ if (cacheHost && url.hostname == cacheHost.hostname) {
22
+ url.host = origin.host;
23
+ url.protocol = origin.protocol;
24
+ }
25
+ const method = request.method.toLowerCase();
26
+ // For non-GET requests, we need to add the request body to the cache key by appending in to the URL query string
27
+ if (method !== 'get') {
28
+ let body = await request.text();
29
+ const bodyParam = url.searchParams.get(constants_1.POST_BODY_QUERY_PARAM);
30
+ if (!bodyParam && body) {
31
+ url.searchParams.set(constants_1.POST_BODY_QUERY_PARAM, encodeURIComponent(body));
32
+ }
33
+ // add method to the cache key so that we can cache requests to the same path with different methods
34
+ url.searchParams.set(constants_1.METHOD_QUERY_PARAM, request.method.toLowerCase());
35
+ }
36
+ // remove layer0_prefetch, layer0_dt_pf and layer0_prefetch_ttl query params so that prefetches and real requests
37
+ // return from the same cache space
38
+ url.searchParams.delete(constants_1.THROTTLED_QUERY_PARAM);
39
+ url.searchParams.delete(constants_1.DEVTOOLS_PREFETCH_QUERY_PARAM);
40
+ url.searchParams.delete(constants_1.PREFETCH_TTL_PARAM);
41
+ // Decode query parameters both when writing to cache and when reading
42
+ // from cache. This ensures the cache is used no matter whether the URL
43
+ // is encoded in either step.
44
+ decodeQueryParams(url);
45
+ return url.toString();
46
+ }
47
+ exports.normalizeRequestUrlForCache = normalizeRequestUrlForCache;
48
+ class PrefetchPlugin {
49
+ constructor({ incrementRead, incrementWrite, cacheHost, prefetchUpstreamRequests, convertToGet, method, }) {
50
+ this.incrementRead = incrementRead;
51
+ this.incrementWrite = incrementWrite;
52
+ this.cacheHost = cacheHost;
53
+ this.prefetchUpstreamRequests = prefetchUpstreamRequests;
54
+ this.convertToGet = convertToGet;
55
+ this.method = method;
56
+ }
57
+ async requestWillFetch({ request }) {
58
+ if (this.convertToGet && this.method !== 'get') {
59
+ const url = new URL(request.url);
60
+ // if the request is not a GET, then it's ok to modify the request and bump
61
+ // the priority since this means it is the real (non-prefetch) request (a
62
+ // prefetch request will not come in as anything but a GET)
63
+ const headers = new Headers(request.headers);
64
+ let body = await request.clone().text();
65
+ url.searchParams.set(constants_1.POST_BODY_QUERY_PARAM, body);
66
+ return new Request(url.toString(), {
67
+ method: 'get',
68
+ headers,
69
+ redirect: request.redirect,
70
+ });
71
+ }
72
+ else {
73
+ // We need to send a clone of the request so that we can read the request body and use
74
+ // it in the cache key in the write phase of the cacheKeyWillBeUsed method.
75
+ return request.clone();
76
+ }
77
+ }
78
+ /**
79
+ * Called when an object is read from or written to the cache. Here we
80
+ * compute cache stats and ensure that ?layer0_prefetch is removed from the cache key so
81
+ * that future requests for prefetched resources will match.
82
+ */
83
+ async cacheKeyWillBeUsed({ request, mode }) {
84
+ if (!isPrefetchRequest(request)) {
85
+ if (mode === 'read') {
86
+ this.incrementRead();
87
+ }
88
+ else {
89
+ this.incrementWrite();
90
+ }
91
+ }
92
+ return normalizeRequestUrlForCache(request, { cacheHost: this.cacheHost });
93
+ }
94
+ /**
95
+ * Called when a response is about to be written to the cache.
96
+ */
97
+ async cacheWillUpdate({ response, request, event }) {
98
+ // If prefetchUpstreamRequests is set, we only prefetch the html pages in SPA mode
99
+ // (e.g Angular) to get the upstream requests response header. We don't actually want
100
+ // to serve the html from the cache, so this returns null:
101
+ if (this.prefetchUpstreamRequests) {
102
+ const backendRequests = response.headers.get(constants_3.BACKEND_REQUESTS_RESPONSE_HEADER_NAME);
103
+ if (backendRequests) {
104
+ const urlsToPrefetch = backendRequests.split(';');
105
+ urlsToPrefetch.forEach(url => {
106
+ if (url)
107
+ prefetch_1.prefetch(url);
108
+ });
109
+ }
110
+ return null;
111
+ }
112
+ // Get the maxage for storing in the service worker cache from x-sw-cache-control
113
+ // Only responses from routes with cache({ browser: { serviceWorkerSeconds: (value) }}) will have this.
114
+ // We should only cache non-prefetch requests if they have browser.serviceWorkerSeconds explicitly set in the router.
115
+ const maxAge = getServiceWorkerMaxAge_1.default(response);
116
+ // Service workers do not handle redirects well. If a redirected response is put in the cache,
117
+ // you'll see this error: "a ServiceWorker passed a redirected Response to FetchEvent.respondWith()
118
+ // while RedirectMode is not ‘follow"
119
+ // Returning null here prevents the response from being cached
120
+ if (response.redirected) {
121
+ log_1.default(`${response.url} was not added to the cache because it was a redirect.`);
122
+ return null;
123
+ // @ts-ignore
124
+ }
125
+ else if (isPrefetchRequest(event.request) || maxAge > 0) {
126
+ log_1.default(`${response.url} was added to the cache with key ${request.url} for ${maxAge || constants_2.DEFAULT_MAX_AGE_SECONDS} seconds.`);
127
+ const headers = new Headers(response.headers);
128
+ headers.set('date', new Date().toUTCString());
129
+ // Change max-age in SW header to custom value from query param
130
+ // If no param is presented the value will be the same
131
+ // This value is read by CustomExpirationPlugin to determinate if the response is still "fresh"
132
+ let swHeaderContent = headers.get(constants_2.SERVICE_WORKER_CACHE_CONTROL);
133
+ swHeaderContent = swHeaderContent
134
+ ? swHeaderContent.replace(/max-age=\d+/, `max-age=${maxAge}`)
135
+ : `max-age=${maxAge}`;
136
+ headers.set(constants_2.SERVICE_WORKER_CACHE_CONTROL, swHeaderContent);
137
+ return new Response(response.body, { ...response, headers });
138
+ }
139
+ else {
140
+ // For routes without an explicit serviceWorkerSeconds cache config, we only cache the
141
+ // response if the request was a prefetch. So in other words, we always write prefetches
142
+ // to the cache.
143
+ log_1.default(`${response.url} was not cached because it was not a prefetch.`);
144
+ return null;
145
+ }
146
+ }
147
+ }
148
+ exports.default = PrefetchPlugin;
149
+ /**
150
+ * Mutates the searchParams object, ensuring that each value is decoded.
151
+ * @param {URL} url The URL whose params should be decoded
152
+ */
153
+ function decodeQueryParams(url) {
154
+ const maybeDecode = (val) => {
155
+ try {
156
+ return decodeURIComponent(val);
157
+ }
158
+ catch (e) {
159
+ // it will fail if the string has a % in it that isn't a URL-decoded entity,
160
+ // so that means the string is already decoded so we can just return the value:
161
+ return val;
162
+ }
163
+ };
164
+ url.searchParams.forEach((param, key) => {
165
+ url.searchParams.set(maybeDecode(key), maybeDecode(param));
166
+ });
167
+ }