@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,209 @@
|
|
|
1
|
+
import { WorkboxPlugin } from 'workbox-core/types';
|
|
2
|
+
import { HTTPMethod } from '@layer0/core/router/CacheOptions';
|
|
3
|
+
import { CustomCacheKeyConfig } from '@layer0/core/router/CustomCacheKey';
|
|
4
|
+
export declare class Stats {
|
|
5
|
+
hits: number;
|
|
6
|
+
misses: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PrefetcherConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Workbox plugins to use when handling a cacheable route.
|
|
11
|
+
*/
|
|
12
|
+
plugins: WorkboxPlugin[];
|
|
13
|
+
/**
|
|
14
|
+
* An optional second domain from which to prefetch instead of the origin domain.
|
|
15
|
+
*/
|
|
16
|
+
cacheHost?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The default time to live for prefetched requests that do not specify a `cache({ browser: { serviceWorkerSeconds }})`
|
|
19
|
+
* config in the router.
|
|
20
|
+
*/
|
|
21
|
+
defaultMaxAgeSeconds?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare type RouteConfig = {
|
|
24
|
+
criteriaPath?: string;
|
|
25
|
+
pattern: string;
|
|
26
|
+
edgeMaxAgeSeconds?: number;
|
|
27
|
+
maxAgeSeconds: number;
|
|
28
|
+
method?: HTTPMethod;
|
|
29
|
+
isPartialPathRoute: boolean;
|
|
30
|
+
key?: CustomCacheKeyConfig[];
|
|
31
|
+
convertToGet?: boolean;
|
|
32
|
+
prefetchUpstreamRequests?: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare type DevtoolsRequestMessage = {
|
|
35
|
+
type: 'layer0-devtools-request';
|
|
36
|
+
timestamp: number;
|
|
37
|
+
request: {
|
|
38
|
+
method: string;
|
|
39
|
+
url: string;
|
|
40
|
+
prefetch: boolean;
|
|
41
|
+
};
|
|
42
|
+
response: {
|
|
43
|
+
ttfb: number;
|
|
44
|
+
status: number;
|
|
45
|
+
headers: {
|
|
46
|
+
[key: string]: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
browserCache: string;
|
|
50
|
+
routeConfig?: RouteConfig;
|
|
51
|
+
};
|
|
52
|
+
export declare type DevtoolsAckMessage = {
|
|
53
|
+
type: 'layer0-devtools-ack';
|
|
54
|
+
bufferedDevtoolMessages: DevtoolsRequestMessage[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Handles prefetching and caching in the the service worker. Responses
|
|
58
|
+
* are cached based on the `browser.serviceWorkerSeconds` cache setting in your routes.
|
|
59
|
+
*
|
|
60
|
+
* **Example routes.js**
|
|
61
|
+
*
|
|
62
|
+
* ```js
|
|
63
|
+
* import { Router } from '@layer0/core/router'
|
|
64
|
+
*
|
|
65
|
+
* module.exports = new Router()
|
|
66
|
+
* .match('/some/path', ({ cache }) => {
|
|
67
|
+
* cache({
|
|
68
|
+
* edge: {
|
|
69
|
+
* maxAgeSeconds: 60 * 60 * 24 // one day
|
|
70
|
+
* },
|
|
71
|
+
* browser: {
|
|
72
|
+
* serviceWorkerSeconds: 60 * 60 // one hour
|
|
73
|
+
* }
|
|
74
|
+
* })
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* **Example Service Worker**
|
|
79
|
+
*
|
|
80
|
+
* ```js
|
|
81
|
+
* import { skipWaiting, clientsClaim } from 'workbox-core'
|
|
82
|
+
* import { Prefetcher } from '@layer0/prefetch/sw'
|
|
83
|
+
* import { precacheAndRoute } from 'workbox-precaching'
|
|
84
|
+
*
|
|
85
|
+
* skipWaiting()
|
|
86
|
+
* clientsClaim()
|
|
87
|
+
* precacheAndRoute(self.__WB_MANIFEST || [])
|
|
88
|
+
*
|
|
89
|
+
* new Prefetcher().route()
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export default class Prefetcher {
|
|
93
|
+
private readonly cacheableResponsePlugin;
|
|
94
|
+
private readonly config;
|
|
95
|
+
private readonly cacheHost?;
|
|
96
|
+
private readonly cacheOrigins;
|
|
97
|
+
private readonly stats;
|
|
98
|
+
private devtoolsEnabled;
|
|
99
|
+
/**
|
|
100
|
+
* bufferedDevtoolMessages keeps track of the request received in the last BUFFER_DELAY_MS.
|
|
101
|
+
*/
|
|
102
|
+
private bufferedDevtoolMessages;
|
|
103
|
+
private clearDevtoolsRequestMessageTimeout?;
|
|
104
|
+
constructor(config?: PrefetcherConfig);
|
|
105
|
+
getRouteConfigsFromManifest(): {
|
|
106
|
+
criteriaPath: any;
|
|
107
|
+
pattern: any;
|
|
108
|
+
method: any;
|
|
109
|
+
maxAgeSeconds: any;
|
|
110
|
+
edgeMaxAgeSeconds: any;
|
|
111
|
+
returnsResponse: any;
|
|
112
|
+
isPartialPathRoute: boolean;
|
|
113
|
+
key: any;
|
|
114
|
+
convertToGet: any;
|
|
115
|
+
prefetchUpstreamRequests: any;
|
|
116
|
+
}[];
|
|
117
|
+
/**
|
|
118
|
+
* Creates a fallback route that ensures that all non-asset requests are handled by the service worker so that
|
|
119
|
+
* we properly cache (or not cache) based on x-sw-cache-control, and that all network requests are seen by the
|
|
120
|
+
* Devtools.
|
|
121
|
+
*/
|
|
122
|
+
private createFallbackRoute;
|
|
123
|
+
/**
|
|
124
|
+
* Adds a fetch event listener that serves content from the browser cache when available. When
|
|
125
|
+
* a request is not in the cache, it will be fetched from the network and added to the
|
|
126
|
+
* cache if the route has `browser.serviceWorkerSeconds` defined in its `cache` setting or if the
|
|
127
|
+
* request is a prefetch.
|
|
128
|
+
*/
|
|
129
|
+
route(): this;
|
|
130
|
+
cache(pattern: RegExp, maxAgeSeconds?: number): this;
|
|
131
|
+
/**
|
|
132
|
+
* Starts listening for messages from the browser
|
|
133
|
+
*/
|
|
134
|
+
private initStats;
|
|
135
|
+
/**
|
|
136
|
+
* Here we implement a handshake protocol between the devtool window client and the service worker:
|
|
137
|
+
* 1. Devtools sends the 'layer0-devtools-syn' event and starts listening for 'layer0-devtools-ack' event
|
|
138
|
+
* 2. Service worker receives the 'layer0-devtools-syn' event and sends the 'layer0-devtools-ack' event
|
|
139
|
+
* along with the buffered messages that were received over the last BUFFER_DELAY_MS.
|
|
140
|
+
* This is because the window widget takes time to download, load and to start listening from
|
|
141
|
+
* Service worker message, while the latter is already receiving http requests.
|
|
142
|
+
* 3. Devtools receives the 'layer0-devtools-ack' event, looks for its current page html request,
|
|
143
|
+
* which corresponds to that tab very first request, and loads requests past that one.
|
|
144
|
+
* 4. Devtools starts listening for live 'layer0-devtools-request' events
|
|
145
|
+
*
|
|
146
|
+
* Notes:
|
|
147
|
+
* - there is no 1-1 channel between window client and Service Worker, so all opened tab
|
|
148
|
+
* will see the 'layer0-devtools-ack' event. Only the ones in handshake phase will handle it.
|
|
149
|
+
*/
|
|
150
|
+
private initDevtoolsHandshakeListener;
|
|
151
|
+
/**
|
|
152
|
+
* Send a postMessage event to all window client.
|
|
153
|
+
* Notes:
|
|
154
|
+
* - When used in multi-tabs, each Devtools will show data from all tabs.
|
|
155
|
+
* That's a bit buggy but it's the best we can do as it's very hard to establish 1-1 channels
|
|
156
|
+
* on ServiceWorker/Client interface, for example the message event from the window does not
|
|
157
|
+
* contain the clientId
|
|
158
|
+
*/
|
|
159
|
+
private sendToAllClients;
|
|
160
|
+
/**
|
|
161
|
+
* Adds a message to this.bufferedDevtoolMessages and manages the buffer so that
|
|
162
|
+
* we don't keep messages more than BUFFER_DELAY_MS.
|
|
163
|
+
*
|
|
164
|
+
* In case of inactivity we clear the whole buffer after BUFFER_DELAY_MS, so technically
|
|
165
|
+
* a message can stay up to 2 x BUFFER_DELAY_MS in the buffer (a bit less than BUFFER_DELAY_MS old
|
|
166
|
+
* when the latest message arrives and cleared BUFFER_DELAY_MS later)
|
|
167
|
+
* @param message
|
|
168
|
+
*/
|
|
169
|
+
private addDevtoolsRequestMessageToBuffer;
|
|
170
|
+
/**
|
|
171
|
+
* Remove message that are too old to stay in devtools message buffer
|
|
172
|
+
*/
|
|
173
|
+
private trimDevtoolsRequestMessageBuffer;
|
|
174
|
+
/**
|
|
175
|
+
* If there's a cacheHost configured check whether the pattern to test
|
|
176
|
+
* against is a partial pathname pattern. If so, check that the origin is
|
|
177
|
+
* also correct: service worker's own origin or cacheHost origin
|
|
178
|
+
* @param pattern A route pattern
|
|
179
|
+
* @param isPartialPathRoute Set to true if the supplied pattern only matches the path, false if it matches the whole URL.
|
|
180
|
+
* @param options.skipAssets Set to true to ignore request that are not 'document' or 'empty' (ie: audio, font, image, ...)
|
|
181
|
+
* @return true if the request should be handled by the service worker.
|
|
182
|
+
*/
|
|
183
|
+
private shouldIntercept;
|
|
184
|
+
/**
|
|
185
|
+
* Creates a DevtoolsPlugin object to be added to a caching strategy instance
|
|
186
|
+
* @param routeConfig The route config for the given route
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
private createDevtoolsPlugin;
|
|
190
|
+
/**
|
|
191
|
+
* Creates a workbox route.
|
|
192
|
+
* @param routeConfig The route pattern and caching options specified within the route definition
|
|
193
|
+
*/
|
|
194
|
+
private createRoute;
|
|
195
|
+
/**
|
|
196
|
+
* A Read is considered a Hit until a Write happens meaning it was a miss
|
|
197
|
+
* This is based on the fragile assumption that a cache read *always* triggers a write
|
|
198
|
+
* in case of cache miss, which is not true if the backend does not respond for example.
|
|
199
|
+
*
|
|
200
|
+
* This could be fixed by checking the presence of `response` in `cachedResponseWillBeUsed``
|
|
201
|
+
* but that leads to other issues as we cannot discard Prefetch requests at that
|
|
202
|
+
* stage.
|
|
203
|
+
* We could implement the same Hack-ish solution as in DevtoolsPlugin to work around that,
|
|
204
|
+
* but those simple stats are probably going away anyway.
|
|
205
|
+
*/
|
|
206
|
+
private incrementRead;
|
|
207
|
+
private incrementWrite;
|
|
208
|
+
private onResponse;
|
|
209
|
+
}
|
package/sw/Prefetcher.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
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 workbox_routing_1 = require("workbox-routing");
|
|
7
|
+
const workbox_strategies_1 = require("workbox-strategies");
|
|
8
|
+
const CustomExpirationPlugin_1 = require("./CustomExpirationPlugin");
|
|
9
|
+
const workbox_cacheable_response_1 = require("workbox-cacheable-response");
|
|
10
|
+
const constants_1 = require("../constants");
|
|
11
|
+
const log_1 = __importDefault(require("./log"));
|
|
12
|
+
const PrefetchPlugin_1 = __importDefault(require("./PrefetchPlugin"));
|
|
13
|
+
const DevtoolsPlugin_1 = __importDefault(require("./DevtoolsPlugin"));
|
|
14
|
+
const messageBrowser_1 = __importDefault(require("./messageBrowser"));
|
|
15
|
+
const CustomCacheKeyPlugin_1 = __importDefault(require("./CustomCacheKeyPlugin"));
|
|
16
|
+
const constants_2 = require("../constants");
|
|
17
|
+
class Stats {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.hits = 0;
|
|
20
|
+
this.misses = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.Stats = Stats;
|
|
24
|
+
const BUFFER_DELAY_MS = 10000;
|
|
25
|
+
/**
|
|
26
|
+
* Handles prefetching and caching in the the service worker. Responses
|
|
27
|
+
* are cached based on the `browser.serviceWorkerSeconds` cache setting in your routes.
|
|
28
|
+
*
|
|
29
|
+
* **Example routes.js**
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
32
|
+
* import { Router } from '@layer0/core/router'
|
|
33
|
+
*
|
|
34
|
+
* module.exports = new Router()
|
|
35
|
+
* .match('/some/path', ({ cache }) => {
|
|
36
|
+
* cache({
|
|
37
|
+
* edge: {
|
|
38
|
+
* maxAgeSeconds: 60 * 60 * 24 // one day
|
|
39
|
+
* },
|
|
40
|
+
* browser: {
|
|
41
|
+
* serviceWorkerSeconds: 60 * 60 // one hour
|
|
42
|
+
* }
|
|
43
|
+
* })
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* **Example Service Worker**
|
|
48
|
+
*
|
|
49
|
+
* ```js
|
|
50
|
+
* import { skipWaiting, clientsClaim } from 'workbox-core'
|
|
51
|
+
* import { Prefetcher } from '@layer0/prefetch/sw'
|
|
52
|
+
* import { precacheAndRoute } from 'workbox-precaching'
|
|
53
|
+
*
|
|
54
|
+
* skipWaiting()
|
|
55
|
+
* clientsClaim()
|
|
56
|
+
* precacheAndRoute(self.__WB_MANIFEST || [])
|
|
57
|
+
*
|
|
58
|
+
* new Prefetcher().route()
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
class Prefetcher {
|
|
62
|
+
constructor(config = { plugins: [] }) {
|
|
63
|
+
// For performance reasons, Devtools plugins are disabled until the first
|
|
64
|
+
// 'layer0-devtools-syn' message is received (ie: a Devtools client is enabled).
|
|
65
|
+
// But once enabled, they will remain active until the Service Worker is reinstalled
|
|
66
|
+
this.devtoolsEnabled = false;
|
|
67
|
+
/**
|
|
68
|
+
* bufferedDevtoolMessages keeps track of the request received in the last BUFFER_DELAY_MS.
|
|
69
|
+
*/
|
|
70
|
+
this.bufferedDevtoolMessages = [];
|
|
71
|
+
this.config = config;
|
|
72
|
+
let manifestURL = '__layer0__/cache-manifest.js';
|
|
73
|
+
this.cacheOrigins = [self.origin];
|
|
74
|
+
if (config.cacheHost) {
|
|
75
|
+
this.cacheHost = new URL(`https://${config.cacheHost}`);
|
|
76
|
+
this.cacheOrigins.push(this.cacheHost.origin);
|
|
77
|
+
manifestURL = this.cacheHost.toString() + manifestURL;
|
|
78
|
+
}
|
|
79
|
+
importScripts(manifestURL);
|
|
80
|
+
this.cacheableResponsePlugin = new workbox_cacheable_response_1.CacheableResponsePlugin({
|
|
81
|
+
statuses: [200, 301, 302],
|
|
82
|
+
});
|
|
83
|
+
this.stats = new Stats();
|
|
84
|
+
this.initStats();
|
|
85
|
+
this.initDevtoolsHandshakeListener();
|
|
86
|
+
}
|
|
87
|
+
getRouteConfigsFromManifest() {
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
const manifest = self.__LAYER0_CACHE_MANIFEST__;
|
|
90
|
+
return manifest.map(({ criteriaPath, route, cacheOptions, method, returnsResponse }) => {
|
|
91
|
+
var _a, _b, _c;
|
|
92
|
+
const maxAgeSeconds = (_a = cacheOptions.browser) === null || _a === void 0 ? void 0 : _a.serviceWorkerSeconds;
|
|
93
|
+
const edgeMaxAgeSeconds = (_b = cacheOptions.edge) === null || _b === void 0 ? void 0 : _b.maxAgeSeconds;
|
|
94
|
+
return {
|
|
95
|
+
criteriaPath,
|
|
96
|
+
pattern: route,
|
|
97
|
+
method,
|
|
98
|
+
maxAgeSeconds,
|
|
99
|
+
edgeMaxAgeSeconds,
|
|
100
|
+
returnsResponse,
|
|
101
|
+
isPartialPathRoute: true,
|
|
102
|
+
key: cacheOptions.key,
|
|
103
|
+
convertToGet: (_c = cacheOptions.browser) === null || _c === void 0 ? void 0 : _c.convertToGet,
|
|
104
|
+
prefetchUpstreamRequests: cacheOptions.prefetchUpstreamRequests,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Creates a fallback route that ensures that all non-asset requests are handled by the service worker so that
|
|
110
|
+
* we properly cache (or not cache) based on x-sw-cache-control, and that all network requests are seen by the
|
|
111
|
+
* Devtools.
|
|
112
|
+
*/
|
|
113
|
+
createFallbackRoute() {
|
|
114
|
+
this.createRoute({
|
|
115
|
+
pattern: '^.*$',
|
|
116
|
+
maxAgeSeconds: 0,
|
|
117
|
+
isPartialPathRoute: true,
|
|
118
|
+
}, true);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Adds a fetch event listener that serves content from the browser cache when available. When
|
|
122
|
+
* a request is not in the cache, it will be fetched from the network and added to the
|
|
123
|
+
* cache if the route has `browser.serviceWorkerSeconds` defined in its `cache` setting or if the
|
|
124
|
+
* request is a prefetch.
|
|
125
|
+
*/
|
|
126
|
+
route() {
|
|
127
|
+
for (const routeConfig of this.getRouteConfigsFromManifest()) {
|
|
128
|
+
const { maxAgeSeconds, prefetchUpstreamRequests } = routeConfig;
|
|
129
|
+
if (
|
|
130
|
+
// Only listen for browser cached routes:
|
|
131
|
+
(maxAgeSeconds && maxAgeSeconds > 0) ||
|
|
132
|
+
// or for routes that return an x-0-upstream-requests header:
|
|
133
|
+
prefetchUpstreamRequests) {
|
|
134
|
+
this.createRoute(routeConfig);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.createFallbackRoute();
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
cache(pattern, maxAgeSeconds = 60 * 60 * 24) {
|
|
141
|
+
this.createRoute({ pattern: pattern.source, maxAgeSeconds, isPartialPathRoute: false });
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Starts listening for messages from the browser
|
|
146
|
+
*/
|
|
147
|
+
initStats() {
|
|
148
|
+
self.addEventListener('message', (e) => {
|
|
149
|
+
if (e.data.action === 'get-stats') {
|
|
150
|
+
messageBrowser_1.default({
|
|
151
|
+
type: 'stats',
|
|
152
|
+
stats: this.stats,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Here we implement a handshake protocol between the devtool window client and the service worker:
|
|
159
|
+
* 1. Devtools sends the 'layer0-devtools-syn' event and starts listening for 'layer0-devtools-ack' event
|
|
160
|
+
* 2. Service worker receives the 'layer0-devtools-syn' event and sends the 'layer0-devtools-ack' event
|
|
161
|
+
* along with the buffered messages that were received over the last BUFFER_DELAY_MS.
|
|
162
|
+
* This is because the window widget takes time to download, load and to start listening from
|
|
163
|
+
* Service worker message, while the latter is already receiving http requests.
|
|
164
|
+
* 3. Devtools receives the 'layer0-devtools-ack' event, looks for its current page html request,
|
|
165
|
+
* which corresponds to that tab very first request, and loads requests past that one.
|
|
166
|
+
* 4. Devtools starts listening for live 'layer0-devtools-request' events
|
|
167
|
+
*
|
|
168
|
+
* Notes:
|
|
169
|
+
* - there is no 1-1 channel between window client and Service Worker, so all opened tab
|
|
170
|
+
* will see the 'layer0-devtools-ack' event. Only the ones in handshake phase will handle it.
|
|
171
|
+
*/
|
|
172
|
+
initDevtoolsHandshakeListener() {
|
|
173
|
+
self.addEventListener('message', async (e) => {
|
|
174
|
+
if (e.data.action === 'layer0-devtools-install') {
|
|
175
|
+
if (!this.devtoolsEnabled) {
|
|
176
|
+
this.devtoolsEnabled = true;
|
|
177
|
+
// Devtools plugin just got initialized so we missed
|
|
178
|
+
// a few queries. We send a message so that the browser
|
|
179
|
+
// refreshes its page.
|
|
180
|
+
this.sendToAllClients({
|
|
181
|
+
type: 'layer0-devtools-ack-installed',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (e.data.action === 'layer0-devtools-syn') {
|
|
186
|
+
if (this.devtoolsEnabled) {
|
|
187
|
+
const ackMessage = {
|
|
188
|
+
type: 'layer0-devtools-ack',
|
|
189
|
+
bufferedDevtoolMessages: this.bufferedDevtoolMessages,
|
|
190
|
+
};
|
|
191
|
+
this.sendToAllClients(ackMessage);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (e.data.action === 'layer0-devtools-clear-cache') {
|
|
195
|
+
caches.keys().then(cacheNames => cacheNames.forEach(name => caches.delete(name)));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Send a postMessage event to all window client.
|
|
201
|
+
* Notes:
|
|
202
|
+
* - When used in multi-tabs, each Devtools will show data from all tabs.
|
|
203
|
+
* That's a bit buggy but it's the best we can do as it's very hard to establish 1-1 channels
|
|
204
|
+
* on ServiceWorker/Client interface, for example the message event from the window does not
|
|
205
|
+
* contain the clientId
|
|
206
|
+
*/
|
|
207
|
+
async sendToAllClients(data) {
|
|
208
|
+
messageBrowser_1.default(data, { broadcast: true });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Adds a message to this.bufferedDevtoolMessages and manages the buffer so that
|
|
212
|
+
* we don't keep messages more than BUFFER_DELAY_MS.
|
|
213
|
+
*
|
|
214
|
+
* In case of inactivity we clear the whole buffer after BUFFER_DELAY_MS, so technically
|
|
215
|
+
* a message can stay up to 2 x BUFFER_DELAY_MS in the buffer (a bit less than BUFFER_DELAY_MS old
|
|
216
|
+
* when the latest message arrives and cleared BUFFER_DELAY_MS later)
|
|
217
|
+
* @param message
|
|
218
|
+
*/
|
|
219
|
+
addDevtoolsRequestMessageToBuffer(message) {
|
|
220
|
+
this.bufferedDevtoolMessages.push(message);
|
|
221
|
+
this.trimDevtoolsRequestMessageBuffer();
|
|
222
|
+
if (this.clearDevtoolsRequestMessageTimeout) {
|
|
223
|
+
clearTimeout(this.clearDevtoolsRequestMessageTimeout);
|
|
224
|
+
}
|
|
225
|
+
// If no additional request for BUFFER_DELAY_MS, we clear the buffer after that
|
|
226
|
+
this.clearDevtoolsRequestMessageTimeout = setTimeout(() => {
|
|
227
|
+
this.bufferedDevtoolMessages = [];
|
|
228
|
+
}, BUFFER_DELAY_MS);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove message that are too old to stay in devtools message buffer
|
|
232
|
+
*/
|
|
233
|
+
trimDevtoolsRequestMessageBuffer() {
|
|
234
|
+
let sliceIndex = 0;
|
|
235
|
+
const sliceMinimumTimestamp = Date.now() - BUFFER_DELAY_MS;
|
|
236
|
+
while (this.bufferedDevtoolMessages[sliceIndex] &&
|
|
237
|
+
this.bufferedDevtoolMessages[sliceIndex].timestamp < sliceMinimumTimestamp) {
|
|
238
|
+
sliceIndex++;
|
|
239
|
+
}
|
|
240
|
+
this.bufferedDevtoolMessages = this.bufferedDevtoolMessages.slice(sliceIndex);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* If there's a cacheHost configured check whether the pattern to test
|
|
244
|
+
* against is a partial pathname pattern. If so, check that the origin is
|
|
245
|
+
* also correct: service worker's own origin or cacheHost origin
|
|
246
|
+
* @param pattern A route pattern
|
|
247
|
+
* @param isPartialPathRoute Set to true if the supplied pattern only matches the path, false if it matches the whole URL.
|
|
248
|
+
* @param options.skipAssets Set to true to ignore request that are not 'document' or 'empty' (ie: audio, font, image, ...)
|
|
249
|
+
* @return true if the request should be handled by the service worker.
|
|
250
|
+
*/
|
|
251
|
+
shouldIntercept(pattern, isPartialPathRoute, skipAssets) {
|
|
252
|
+
return ({ url, request }) => {
|
|
253
|
+
if (skipAssets &&
|
|
254
|
+
request.destination &&
|
|
255
|
+
!['document', 'empty'].includes(request.destination)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (isPartialPathRoute) {
|
|
259
|
+
return this.cacheOrigins.includes(url.origin) && pattern.test(url.pathname);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
return pattern.test(url.toString());
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Creates a DevtoolsPlugin object to be added to a caching strategy instance
|
|
268
|
+
* @param routeConfig The route config for the given route
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
createDevtoolsPlugin(routeConfig) {
|
|
272
|
+
return new DevtoolsPlugin_1.default({
|
|
273
|
+
onResponse: this.onResponse.bind(this),
|
|
274
|
+
isEnabledFn: () => this.devtoolsEnabled,
|
|
275
|
+
routeConfig,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Creates a workbox route.
|
|
280
|
+
* @param routeConfig The route pattern and caching options specified within the route definition
|
|
281
|
+
*/
|
|
282
|
+
createRoute(routeConfig, skipAssets = false) {
|
|
283
|
+
const { pattern, method: optMethod, maxAgeSeconds, isPartialPathRoute, key, convertToGet, prefetchUpstreamRequests, } = routeConfig;
|
|
284
|
+
const method = (optMethod || 'get').toLowerCase();
|
|
285
|
+
const patternRegexp = new RegExp(pattern, 'i');
|
|
286
|
+
log_1.default(`[route] ${method} ${pattern}, maxAgeSeconds: ${maxAgeSeconds}`);
|
|
287
|
+
// all routes must have a GET route registered, so Workbox can properly look
|
|
288
|
+
// it up when running workbox-routing/Router#findMatchingRoute:
|
|
289
|
+
if (method !== 'get') {
|
|
290
|
+
this.createRoute({ ...routeConfig, method: 'get' }, skipAssets);
|
|
291
|
+
}
|
|
292
|
+
const prefetchPlugin = new PrefetchPlugin_1.default({
|
|
293
|
+
incrementRead: this.incrementRead.bind(this),
|
|
294
|
+
incrementWrite: this.incrementWrite.bind(this),
|
|
295
|
+
cacheHost: this.cacheHost,
|
|
296
|
+
prefetchUpstreamRequests,
|
|
297
|
+
convertToGet,
|
|
298
|
+
method,
|
|
299
|
+
});
|
|
300
|
+
const ttl = maxAgeSeconds || this.config.defaultMaxAgeSeconds || constants_2.DEFAULT_MAX_AGE_SECONDS;
|
|
301
|
+
const plugins = [
|
|
302
|
+
this.cacheableResponsePlugin,
|
|
303
|
+
new CustomExpirationPlugin_1.CustomExpirationPlugin({ maxAgeSeconds: ttl }),
|
|
304
|
+
// DevtoolPlugin needs to be injected before PrefetchPlugin as this later
|
|
305
|
+
// changes request url to normalise the cache key
|
|
306
|
+
this.createDevtoolsPlugin(routeConfig),
|
|
307
|
+
prefetchPlugin,
|
|
308
|
+
...(this.config.plugins || []),
|
|
309
|
+
];
|
|
310
|
+
if (key) {
|
|
311
|
+
plugins.push(new CustomCacheKeyPlugin_1.default(patternRegexp, key));
|
|
312
|
+
}
|
|
313
|
+
workbox_routing_1.registerRoute(this.shouldIntercept(patternRegexp, isPartialPathRoute, skipAssets), new workbox_strategies_1.CacheFirst({
|
|
314
|
+
cacheName: constants_1.CACHE_NAME,
|
|
315
|
+
matchOptions: {
|
|
316
|
+
ignoreVary: true,
|
|
317
|
+
},
|
|
318
|
+
plugins,
|
|
319
|
+
}),
|
|
320
|
+
// @ts-ignore
|
|
321
|
+
method.toUpperCase() // HTTPMethods map directly from @layer0/core lowercase => @workbox uppercase
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A Read is considered a Hit until a Write happens meaning it was a miss
|
|
326
|
+
* This is based on the fragile assumption that a cache read *always* triggers a write
|
|
327
|
+
* in case of cache miss, which is not true if the backend does not respond for example.
|
|
328
|
+
*
|
|
329
|
+
* This could be fixed by checking the presence of `response` in `cachedResponseWillBeUsed``
|
|
330
|
+
* but that leads to other issues as we cannot discard Prefetch requests at that
|
|
331
|
+
* stage.
|
|
332
|
+
* We could implement the same Hack-ish solution as in DevtoolsPlugin to work around that,
|
|
333
|
+
* but those simple stats are probably going away anyway.
|
|
334
|
+
*/
|
|
335
|
+
incrementRead() {
|
|
336
|
+
this.stats.hits++;
|
|
337
|
+
}
|
|
338
|
+
incrementWrite() {
|
|
339
|
+
this.stats.misses++;
|
|
340
|
+
this.stats.hits--;
|
|
341
|
+
}
|
|
342
|
+
async onResponse({ request, response, ttfb, prefetch, browserCache, routeConfig, }) {
|
|
343
|
+
const headers = {};
|
|
344
|
+
response.headers.forEach((value, key) => {
|
|
345
|
+
headers[key] = value;
|
|
346
|
+
});
|
|
347
|
+
const devToolMessage = {
|
|
348
|
+
type: 'layer0-devtools-request',
|
|
349
|
+
timestamp: Date.now(),
|
|
350
|
+
request: {
|
|
351
|
+
method: request.method,
|
|
352
|
+
url: request.url,
|
|
353
|
+
prefetch,
|
|
354
|
+
},
|
|
355
|
+
response: {
|
|
356
|
+
ttfb,
|
|
357
|
+
status: response.status,
|
|
358
|
+
headers,
|
|
359
|
+
},
|
|
360
|
+
browserCache,
|
|
361
|
+
routeConfig,
|
|
362
|
+
};
|
|
363
|
+
this.sendToAllClients(devToolMessage);
|
|
364
|
+
this.addDevtoolsRequestMessageToBuffer(devToolMessage);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
exports.default = Prefetcher;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the max age in seconds specified in the x-sw-cache-control response header.
|
|
3
|
+
* If no header or max-age value is present, returns null
|
|
4
|
+
* @param response
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
export declare function getServiceWorkerMaxAgeFromHeader(response: Response): number | null;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the max age in seconds specified in custom query param.
|
|
10
|
+
* If no custom param is present, returns null
|
|
11
|
+
* @param response
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export declare function getServiceWorkerMaxAgeFromParam(response: Response): number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the max age in seconds specified in custom query param.
|
|
17
|
+
* If no custom param is present, returns the max age in seconds specified in the x-sw-cache-control response header.
|
|
18
|
+
* If no header or max-age value is present, this function returns 0.
|
|
19
|
+
* @param response
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export default function getServiceWorkerMaxAge(response: Response): number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const constants_1 = require("../constants");
|
|
4
|
+
const constants_2 = require("@layer0/core/constants");
|
|
5
|
+
/**
|
|
6
|
+
* Returns the max age in seconds specified in the x-sw-cache-control response header.
|
|
7
|
+
* If no header or max-age value is present, returns null
|
|
8
|
+
* @param response
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
function getServiceWorkerMaxAgeFromHeader(response) {
|
|
12
|
+
const header = response.headers.get(constants_1.SERVICE_WORKER_CACHE_CONTROL);
|
|
13
|
+
if (header) {
|
|
14
|
+
for (let pair of header.split(',')) {
|
|
15
|
+
const [key, value] = pair.split('=');
|
|
16
|
+
if (key === 'max-age') {
|
|
17
|
+
return parseInt(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
exports.getServiceWorkerMaxAgeFromHeader = getServiceWorkerMaxAgeFromHeader;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the max age in seconds specified in custom query param.
|
|
26
|
+
* If no custom param is present, returns null
|
|
27
|
+
* @param response
|
|
28
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
function getServiceWorkerMaxAgeFromParam(response) {
|
|
31
|
+
var _a;
|
|
32
|
+
if (((_a = response === null || response === void 0 ? void 0 : response.url) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
33
|
+
const customMaxAgeSeconds = new URL(response.url).searchParams.get(constants_2.PREFETCH_TTL_PARAM);
|
|
34
|
+
if (customMaxAgeSeconds) {
|
|
35
|
+
return parseInt(customMaxAgeSeconds);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
exports.getServiceWorkerMaxAgeFromParam = getServiceWorkerMaxAgeFromParam;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the max age in seconds specified in custom query param.
|
|
43
|
+
* If no custom param is present, returns the max age in seconds specified in the x-sw-cache-control response header.
|
|
44
|
+
* If no header or max-age value is present, this function returns 0.
|
|
45
|
+
* @param response
|
|
46
|
+
* @returns
|
|
47
|
+
*/
|
|
48
|
+
function getServiceWorkerMaxAge(response) {
|
|
49
|
+
return (getServiceWorkerMaxAgeFromParam(response) || getServiceWorkerMaxAgeFromHeader(response) || 0);
|
|
50
|
+
}
|
|
51
|
+
exports.default = getServiceWorkerMaxAge;
|
package/sw/index.d.ts
ADDED
package/sw/index.js
ADDED
package/sw/log.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function log(...message: any[]): void;
|
package/sw/log.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* istanbul ignore file */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
function log(...message) {
|
|
5
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
6
|
+
console.log('%c@layer0/prefetch', 'background: #43a047; color: #ffffff; font-weight:bold; padding: 3px 5px; border-radius: 5px', ...message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
exports.default = log;
|