From baeacfb170fef27ba66f9574e92d9ea624e5f387 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 9 Mar 2024 17:05:37 +0100 Subject: [PATCH] Allow multiple page cache --- assets/src/installation-status_controller.js | 54 +++++++ src/Command/ListCacheStrategiesCommand.php | 2 +- src/Dto/PageCache.php | 6 +- src/Dto/Workbox.php | 7 +- .../config/definition/service_worker.php | 136 ++++++++++-------- src/Service/Rule/AssetCache.php | 4 +- src/Service/Rule/BackgroundSync.php | 5 +- src/Service/Rule/GoogleFontCache.php | 6 +- .../Rule/{PageCache.php => PageCaches.php} | 98 +++++++------ 9 files changed, 198 insertions(+), 120 deletions(-) create mode 100644 assets/src/installation-status_controller.js rename src/Service/Rule/{PageCache.php => PageCaches.php} (57%) diff --git a/assets/src/installation-status_controller.js b/assets/src/installation-status_controller.js new file mode 100644 index 0000000..dc180d6 --- /dev/null +++ b/assets/src/installation-status_controller.js @@ -0,0 +1,54 @@ +'use strict'; + +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['message', 'attribute']; + static values = { + installedMessage: { type: String, default: 'The application is installed' }, + notInstalledMessage: { type: String, default: 'The application is not installed' }, + }; + + connect = () => { + console.log(window.matchMedia('(display-mode: standalone)').matches) + this.dispatchEvent('connect', {}); + if (navigator.onLine) { + this.statusChanged({ + status: 'ONLINE', + message: this.onlineMessageValue, + }); + } else { + this.statusChanged({ + status: 'OFFLINE', + message: this.offlineMessageValue, + }); + } + + window.addEventListener('online', () => { + this.statusChanged({ + status: 'ONLINE', + message: this.onlineMessageValue, + }); + }); + window.addEventListener('offline', () => { + this.statusChanged({ + status: 'OFFLINE', + message: this.offlineMessageValue, + }); + }); + } + dispatchEvent = (name, payload) => { + this.dispatch(name, { detail: payload, prefix: 'connection-status' }); + } + + statusChanged = (data) => { + this.messageTargets.forEach((element) => { + element.innerHTML = data.message; + }); + this.attributeTargets.forEach((element) => { + element.setAttribute('data-connection-status', data.status); + }); + this.dispatchEvent('status-changed', { detail: data }); + } +} diff --git a/src/Command/ListCacheStrategiesCommand.php b/src/Command/ListCacheStrategiesCommand.php index 2bf853f..6f79a29 100644 --- a/src/Command/ListCacheStrategiesCommand.php +++ b/src/Command/ListCacheStrategiesCommand.php @@ -42,7 +42,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $strategy->urlPattern, $strategy->enabled ? 'Yes' : 'No', $strategy->requireWorkbox ? 'Yes' : 'No', - Yaml::dump($strategy->options), + '', //Yaml::dump($strategy->options), ]); } } diff --git a/src/Dto/PageCache.php b/src/Dto/PageCache.php index 63b501e..43270d7 100644 --- a/src/Dto/PageCache.php +++ b/src/Dto/PageCache.php @@ -8,13 +8,11 @@ final class PageCache { - public bool $enabled = true; - #[SerializedName('cache_name')] - public string $cacheName = 'pages'; + public string $cacheName; #[SerializedName('regex')] - public string $regex = '/\.(ico|png|jpe?g|gif|svg|webp|bmp)$/'; + public string $regex; #[SerializedName('network_timeout')] public int $networkTimeout = 3; diff --git a/src/Dto/Workbox.php b/src/Dto/Workbox.php index 4b4a132..0e25b10 100644 --- a/src/Dto/Workbox.php +++ b/src/Dto/Workbox.php @@ -27,8 +27,11 @@ final class Workbox #[SerializedName('font_cache')] public FontCache $fontCache; - #[SerializedName('page_cache')] - public PageCache $pageCache; + /** + * @var array + */ + #[SerializedName('page_caches')] + public array $pageCaches; #[SerializedName('asset_cache')] public AssetCache $assetCache; diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index 92ff563..a487015 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -97,11 +97,11 @@ ->beforeNormalization() ->ifTrue(static fn (mixed $v): bool => true) ->then(static function (mixed $v): array { - if (isset($v['page_cache'])) { + if (isset($v['page_caches'])) { return $v; } - $v['page_cache'] = array_filter([ - 'enabled' => true, + $v['page_caches'] = []; + $v['page_caches'][] = array_filter([ 'cache_name' => $v['page_cache_name'] ?? 'pages', 'network_timeout' => $v['network_timeout_seconds'] ?? 3, 'urls' => $v['warm_cache_urls'] ?? [], @@ -281,64 +281,74 @@ ->end() ->end() ->end() - ->arrayNode('page_cache') - ->canBeDisabled() - ->children() - ->scalarNode('cache_name') - ->defaultValue('pages') - ->info('The name of the page cache.') - ->end() - ->integerNode('network_timeout') - ->defaultValue(3) - ->info( - 'The network timeout in seconds before cache is called (for warm cache URLs only).' - ) - ->example([1, 2, 5]) - ->end() - ->scalarNode('strategy') - ->defaultValue('networkFirst') - ->info( - 'The caching strategy. Only "networkFirst" and "staleWhileRevalidate" are supported.' - ) - ->example(['networkFirst', 'staleWhileRevalidate']) - ->end() - ->booleanNode('broadcast') - ->defaultFalse() - ->info( - 'Whether to broadcast the cache update events. Only supported with "staleWhileRevalidate" strategy.' - ) - ->end() - ->arrayNode('broadcast_headers') - ->treatNullLike(['Content-Length', 'ETag', 'Last-Modified']) - ->treatFalseLike(['Content-Length', 'ETag', 'Last-Modified']) - ->treatTrueLike(['Content-Length', 'ETag', 'Last-Modified']) - ->defaultValue(['Content-Length', 'ETag', 'Last-Modified']) - ->scalarPrototype()->end() - ->end() - ->arrayNode('urls') - ->treatNullLike([]) - ->treatFalseLike([]) - ->treatTrueLike([]) - ->info('The URLs to warm the cache. The URLs shall be served by the application.') - ->arrayPrototype() - ->beforeNormalization() - ->ifString() - ->then(static fn (string $v): array => [ - 'path' => $v, - ]) - ->end() - ->children() - ->scalarNode('path') - ->isRequired() - ->info('The URL of the shortcut.') - ->example('app_homepage') + ->arrayNode('page_caches') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->arrayPrototype() + ->children() + ->scalarNode('regex') + ->isRequired() + ->info('The regex to match the URLs.') + ->end() + ->scalarNode('cache_name') + ->isRequired() + ->info('The name of the page cache.') + ->end() + ->integerNode('network_timeout') + ->defaultValue(3) + ->info( + 'The network timeout in seconds before cache is called (for "networkFirst" strategy only).' + ) + ->example([1, 2, 5]) + ->end() + ->scalarNode('strategy') + ->defaultValue('networkFirst') + ->info( + 'The caching strategy. Only "networkFirst" and "staleWhileRevalidate" are supported.' + ) + ->example(['networkFirst', 'staleWhileRevalidate']) + ->end() + ->booleanNode('broadcast') + ->defaultFalse() + ->info( + 'Whether to broadcast the cache update events (for "staleWhileRevalidate" strategy only).' + ) + ->end() + ->arrayNode('broadcast_headers') + ->treatNullLike(['Content-Length', 'ETag', 'Last-Modified']) + ->treatFalseLike(['Content-Length', 'ETag', 'Last-Modified']) + ->treatTrueLike(['Content-Length', 'ETag', 'Last-Modified']) + ->defaultValue(['Content-Length', 'ETag', 'Last-Modified']) + ->scalarPrototype()->end() + ->end() + ->arrayNode('urls') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->info( + 'The URLs to warm the cache. The URLs shall be served by the application.' + ) + ->arrayPrototype() + ->beforeNormalization() + ->ifString() + ->then(static fn (string $v): array => [ + 'path' => $v, + ]) ->end() - ->arrayNode('params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action.') + ->children() + ->scalarNode('path') + ->isRequired() + ->info('The URL of the shortcut.') + ->example('app_homepage') + ->end() + ->arrayNode('params') + ->treatFalseLike([]) + ->treatTrueLike([]) + ->treatNullLike([]) + ->prototype('variable')->end() + ->info('The parameters of the action.') + ->end() ->end() ->end() ->end() @@ -409,7 +419,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.cache_name" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].cache_name" instead.' ) ->end() ->scalarNode('asset_cache_name') @@ -501,7 +511,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.network_timeout" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].network_timeout" instead.' ) ->end() ->arrayNode('warm_cache_urls') @@ -512,7 +522,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.urls" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].urls" instead.' ) ->arrayPrototype() ->beforeNormalization() diff --git a/src/Service/Rule/AssetCache.php b/src/Service/Rule/AssetCache.php index a77bb3c..6a5a3e0 100644 --- a/src/Service/Rule/AssetCache.php +++ b/src/Service/Rule/AssetCache.php @@ -67,7 +67,7 @@ public function process(string $body): string $assetUrlsLength = count($assets) * 2; $declaration = <<workbox->assetCache->cacheName}', plugins: [ new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}), @@ -103,7 +103,7 @@ public function getCacheStrategies(): array return [ CacheStrategy::create( $this->workbox->assetCache->cacheName, - CacheStrategy::STRATEGY_CACHE_FIRST, + CacheStrategy::STRATEGY_CACHE_ONLY, sprintf("'({url}) => url.pathname.startsWith('%s')'", $this->assetPublicPrefix), $this->workbox->enabled && $this->workbox->assetCache->enabled, true, diff --git a/src/Service/Rule/BackgroundSync.php b/src/Service/Rule/BackgroundSync.php index bee1ac4..2abaf1b 100644 --- a/src/Service/Rule/BackgroundSync.php +++ b/src/Service/Rule/BackgroundSync.php @@ -69,15 +69,12 @@ public function getCacheStrategies(): array $strategies = []; foreach ($this->workbox->backgroundSync as $sync) { $strategies[] = CacheStrategy::create( - '---', + 'BackgroundSync API', CacheStrategy::STRATEGY_NETWORK_ONLY, $sync->regex, $this->workbox->enabled, true, [ - 'maxTimeout' => 0, - 'maxAge' => 0, - 'maxEntries' => 0, 'plugins' => [ sprintf('backgroundSync: "%s"', $sync->queueName), sprintf('broadcastChannel: "%s"', $sync->broadcastChannel ?? '---'), diff --git a/src/Service/Rule/GoogleFontCache.php b/src/Service/Rule/GoogleFontCache.php index 12a0ed8..a5ccca1 100644 --- a/src/Service/Rule/GoogleFontCache.php +++ b/src/Service/Rule/GoogleFontCache.php @@ -77,7 +77,11 @@ public function getCacheStrategies(): array 'workbox.recipes.googleFontsCache', 'Google Fonts Cache', $this->workbox->enabled && $this->workbox->googleFontCache->enabled, - $this->workbox->googleFontCache->enabled + $this->workbox->googleFontCache->enabled, + [ + 'maxAge' => $this->workbox->googleFontCache->maxAge, + 'maxEntries' => $this->workbox->googleFontCache->maxEntries, + ] ), ]; } diff --git a/src/Service/Rule/PageCache.php b/src/Service/Rule/PageCaches.php similarity index 57% rename from src/Service/Rule/PageCache.php rename to src/Service/Rule/PageCaches.php index 54f1abc..fb8356e 100644 --- a/src/Service/Rule/PageCache.php +++ b/src/Service/Rule/PageCaches.php @@ -4,6 +4,7 @@ namespace SpomkyLabs\PwaBundle\Service\Rule; +use SpomkyLabs\PwaBundle\Dto\PageCache; use SpomkyLabs\PwaBundle\Dto\ServiceWorker; use SpomkyLabs\PwaBundle\Dto\Workbox; use SpomkyLabs\PwaBundle\Service\CacheStrategy; @@ -18,7 +19,7 @@ use const JSON_UNESCAPED_UNICODE; use const PHP_EOL; -final readonly class PageCache implements ServiceWorkerRule, HasCacheStrategies +final readonly class PageCaches implements ServiceWorkerRule, HasCacheStrategies { /** * @var array @@ -50,42 +51,81 @@ public function process(string $body): string if ($this->workbox->enabled === false) { return $body; } - if ($this->workbox->pageCache->enabled === false) { - return $body; + + foreach (array_values($this->workbox->pageCaches) as $id => $pageCache) { + $body = $this->processPageCache($id, $pageCache, $body); + } + + return $body; + } + + public function getCacheStrategies(): array + { + $strategies = []; + foreach ($this->workbox->pageCaches as $pageCache) { + $strategy = match ($pageCache->strategy) { + 'staleWhileRevalidate' => CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE, + default => CacheStrategy::STRATEGY_NETWORK_FIRST, + }; + $plugins = ['CacheableResponsePlugin']; + if ($pageCache->broadcast === true && $strategy === CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE) { + $plugins[] = 'BroadcastUpdatePlugin'; + } + $routes = $this->serializer->serialize($pageCache->urls, 'json', $this->jsonOptions); + $url = json_decode($routes, true, 512, JSON_THROW_ON_ERROR); + $strategies[] = + CacheStrategy::create( + $pageCache->cacheName, + $strategy, + $pageCache->regex, + $this->workbox->enabled, + true, + [ + 'maxTimeout' => $pageCache->networkTimeout, + 'plugins' => $plugins, + 'warmUrls' => $url, + ] + ); } - $routes = $this->serializer->serialize($this->workbox->pageCache->urls, 'json', $this->jsonOptions); - $strategy = match ($this->workbox->pageCache->strategy) { + + return $strategies; + } + + private function processPageCache(int $id, PageCache $pageCache, string $body): string + { + $routes = $this->serializer->serialize($pageCache->urls, 'json', $this->jsonOptions); + $strategy = match ($pageCache->strategy) { 'staleWhileRevalidate' => 'StaleWhileRevalidate', default => 'NetworkFirst', }; $broadcastHeaders = json_encode( - $this->workbox->pageCache->broadcastHeaders === [] ? [ + $pageCache->broadcastHeaders === [] ? [ 'Content-Type', 'ETag', 'Last-Modified', - ] : $this->workbox->pageCache->broadcastHeaders, + ] : $pageCache->broadcastHeaders, JSON_THROW_ON_ERROR, 512 ); - $broadcastUpdate = ($strategy === 'StaleWhileRevalidate' && $this->workbox->pageCache->broadcast === true) ? sprintf( + $broadcastUpdate = ($strategy === 'StaleWhileRevalidate' && $pageCache->broadcast === true) ? sprintf( ',new workbox.broadcastUpdate.BroadcastUpdatePlugin({headersToCheck: %s})', $broadcastHeaders ) : ''; $declaration = <<workbox->pageCache->networkTimeout}, - cacheName: '{$this->workbox->pageCache->cacheName}', +const pageCache{$id}Strategy = new workbox.strategies.{$strategy}({ + networkTimeoutSeconds: {$pageCache->networkTimeout}, + cacheName: '{$pageCache->cacheName}', plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}){$broadcastUpdate}], }); workbox.routing.registerRoute( - ({request}) => request.mode === 'navigate', - pageCacheStrategy + new RegExp('{$pageCache->regex}'), + pageCache{$id}Strategy ); self.addEventListener('install', event => { const done = {$routes}.map( path => - pageCacheStrategy.handleAll({ + pageCache{$id}Strategy.handleAll({ event, request: new Request(path), })[1] @@ -100,7 +140,7 @@ public function process(string $body): string const urls = event.data.payload.urls || []; const done = urls.map( path => - pageCacheStrategy.handleAll({ + pageCache{$id}Strategy.handleAll({ event, request: new Request(path), })[1] @@ -112,32 +152,4 @@ public function process(string $body): string return $body . PHP_EOL . PHP_EOL . trim($declaration); } - - public function getCacheStrategies(): array - { - $strategy = match ($this->workbox->pageCache->strategy) { - 'staleWhileRevalidate' => CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE, - default => CacheStrategy::STRATEGY_NETWORK_FIRST, - }; - $plugins = ['CacheableResponsePlugin']; - if ($this->workbox->pageCache->broadcast === true && $strategy === CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE) { - $plugins[] = 'BroadcastUpdatePlugin'; - } - $routes = $this->serializer->serialize($this->workbox->pageCache->urls, 'json', $this->jsonOptions); - $url = json_decode($routes, true, 512, JSON_THROW_ON_ERROR); - return [ - CacheStrategy::create( - $this->workbox->pageCache->cacheName, - $strategy, - "({request}) => request.mode === 'navigate'", - $this->workbox->enabled && $this->workbox->pageCache->enabled, - true, - [ - 'maxTimeout' => $this->workbox->pageCache->networkTimeout, - 'plugins' => $plugins, - 'warmUrls' => $url, - ] - ), - ]; - } }