@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.
- package/README.md +7 -0
- package/constants.d.ts +23 -0
- package/constants.js +25 -0
- package/index.js +24 -0
- package/package.json +50 -0
- package/sw/CustomCacheKeyPlugin.d.ts +10 -0
- package/sw/CustomCacheKeyPlugin.js +54 -0
- package/sw/CustomExpirationPlugin.d.ts +122 -0
- package/sw/CustomExpirationPlugin.js +280 -0
- package/sw/DeepFetchPlugin.d.ts +108 -0
- package/sw/DeepFetchPlugin.js +144 -0
- package/sw/DevtoolsPlugin.d.ts +64 -0
- package/sw/DevtoolsPlugin.js +131 -0
- package/sw/PrefetchPlugin.d.ts +32 -0
- package/sw/PrefetchPlugin.js +167 -0
- package/sw/Prefetcher.d.ts +209 -0
- package/sw/Prefetcher.js +367 -0
- package/sw/getServiceWorkerMaxAge.d.ts +22 -0
- package/sw/getServiceWorkerMaxAge.js +51 -0
- package/sw/index.d.ts +2 -0
- package/sw/index.js +6 -0
- package/sw/log.d.ts +1 -0
- package/sw/log.js +9 -0
- package/sw/messageBrowser.d.ts +7 -0
- package/sw/messageBrowser.js +18 -0
- package/sw/prefetch.d.ts +9 -0
- package/sw/prefetch.js +17 -0
- package/window/InstallOptions.d.ts +77 -0
- package/window/InstallOptions.js +2 -0
- package/window/PrefetchOptions.d.ts +19 -0
- package/window/PrefetchOptions.js +2 -0
- package/window/entry.d.ts +1 -0
- package/window/entry.js +8 -0
- package/window/getHostName.d.ts +1 -0
- package/window/getHostName.js +6 -0
- package/window/getUserAgent.d.ts +1 -0
- package/window/getUserAgent.js +6 -0
- package/window/index.d.ts +3 -0
- package/window/index.js +8 -0
- package/window/install.d.ts +13 -0
- package/window/install.js +72 -0
- package/window/isSafari.d.ts +6 -0
- package/window/isSafari.js +31 -0
- package/window/loadManifest.d.ts +3 -0
- package/window/loadManifest.js +20 -0
- package/window/prefetch.d.ts +71 -0
- package/window/prefetch.js +157 -0
- package/window/prefetchWhenVisible.d.ts +5 -0
- package/window/prefetchWhenVisible.js +20 -0
- package/window/registerServiceWorker.d.ts +2 -0
- package/window/registerServiceWorker.js +66 -0
- package/window/runWhenVisible.d.ts +6 -0
- package/window/runWhenVisible.js +28 -0
- package/window/waitForServiceWorker.d.ts +4 -0
- package/window/waitForServiceWorker.js +27 -0
- package/window/watchElements.d.ts +7 -0
- package/window/watchElements.js +63 -0
- package/window/watchLinks.d.ts +9 -0
- 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
|
+
}
|