@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,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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ export { default as Prefetcher } from './Prefetcher';
2
+ export { prefetch } from './prefetch';
package/sw/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var Prefetcher_1 = require("./Prefetcher");
4
+ exports.Prefetcher = Prefetcher_1.default;
5
+ var prefetch_1 = require("./prefetch");
6
+ exports.prefetch = prefetch_1.prefetch;
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;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sends a message to the last focused browser window.
3
+ * @param message
4
+ */
5
+ export default function messageBrowser(message: any, { broadcast }?: {
6
+ broadcast?: boolean;
7
+ }): any;