From 28663a4e022ba43e507eaa876d163f9848bf4259 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 14 Feb 2025 20:27:39 +0100 Subject: [PATCH] Add background fetch support to service worker Introduce DTO, JS controller, and service worker rule to handle background fetch functionality. This includes configuration options and UI updates for progress, success, and failure states, enabling dynamic service worker behavior. --- assets/package.json | 7 ++ assets/src/backgroundfetch_controller.js | 93 ++++++++++++++++ phpstan-baseline.neon | 80 ++++++++++++-- src/Dto/BackgroundFetch.php | 24 +++++ src/Dto/ServiceWorker.php | 3 + .../config/definition/service_worker.php | 17 +++ .../BackgroundFetchCache.php | 101 ++++++++++++++++++ 7 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 assets/src/backgroundfetch_controller.js create mode 100644 src/Dto/BackgroundFetch.php create mode 100644 src/ServiceWorkerRule/BackgroundFetchCache.php diff --git a/assets/package.json b/assets/package.json index 9989912..e89b2c4 100644 --- a/assets/package.json +++ b/assets/package.json @@ -5,6 +5,13 @@ "version": "1.0.0", "symfony": { "controllers": { + "backgroundfetch": { + "main": "src/backgroundfetch_controller.js", + "name": "pwa/backgroundfetch", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true + }, "backgroundsync-form": { "main": "src/backgroundsync-form_controller.js", "name": "pwa/backgroundsync-form", diff --git a/assets/src/backgroundfetch_controller.js b/assets/src/backgroundfetch_controller.js new file mode 100644 index 0000000..1817cb6 --- /dev/null +++ b/assets/src/backgroundfetch_controller.js @@ -0,0 +1,93 @@ +'use strict'; + +import Controller from './abstract_controller.js'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static values = { + id: { type: String }, + url: { type: String }, + cacheName: { type: String, default: null }, + persistentStorage: { type: Boolean, default: false }, + title: { type: String, default: undefined }, + downloadTotal: { type: Number, default: undefined }, + icons: { type: String, default: undefined }, + }; + cache = null; + + connect = async () => { + if (!this.idValue) { + console.error("No ID provided"); + return; + } + if (!this.urlValue) { + console.error("No URL provided"); + return; + } + if (this.persistentStorageValue && navigator.storage && navigator.storage.persist) { + navigator.storage.persist(); + } + if (this.cacheNameValue) { + this.cache = await caches.open(this.cacheNameValue); + } + await this.state(); + } + + state = async () => { + const payload = { + id: this.idValue, + url: this.urlValue, + } + if (!this.cache) { + this.dispatchEvent('missing', payload); + return 'missing'; + } + const isInCache = await this.cache.match(this.urlValue); + if (!isInCache) { + this.dispatchEvent('missing', payload); + return 'missing'; + } + this.dispatchEvent('cached', payload); + return 'cached'; + } + + download = async ({params}) => { + const state = await this.state(); + if (state === 'cached' && !params.force) { + return; + } + const registration = await navigator.serviceWorker.ready; + const bgFetch = await registration.backgroundFetch.fetch(this.idValue, [this.urlValue], { + title: this.titleValue, + icons: JSON.parse(this.iconsValue ??'[]'), + downloadTotal: this.downloadTotalValue, + }); + bgFetch.addEventListener('progress', () => this.dispatchStatus(bgFetch)); + } + + dispatchStatus = async (bgFetch) => { + this.dispatchEvent('progress', { + id: bgFetch.id, + uploadTotal: bgFetch.uploadTotal, + uploaded: bgFetch.uploaded, + downloadTotal: bgFetch.downloadTotal, + downloaded: bgFetch.downloaded, + result: bgFetch.result, + failureReason: bgFetch.failureReason, + recordsAvailable: bgFetch.recordsAvailable, + }); + if (!bgFetch.recordsAvailable || !this.cache) { + return; + } + const records = await bgFetch.matchAll(); + const promises = records.map(async record => { + const response = await record.responseReady; + await this.cache.put(record.request, response); + this.dispatchEvent('cached', { + id: this.idValue, + url: this.urlValue, + }); + }); + await Promise.all(promises); + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 86209cd..2ab85e3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -402,6 +402,18 @@ parameters: count: 1 path: src/DataCollector/PwaCollector.php + - + message: '#^Class SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch has an uninitialized property \$cacheName\. Give it default value or assign it in the constructor\.$#' + identifier: property.uninitialized + count: 1 + path: src/Dto/BackgroundFetch.php + + - + message: '#^Class SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch has an uninitialized property \$enabled\. Give it default value or assign it in the constructor\.$#' + identifier: property.uninitialized + count: 1 + path: src/Dto/BackgroundFetch.php + - message: '#^Class SpomkyLabs\\PwaBundle\\Dto\\BackgroundSync has an uninitialized property \$forceSyncFallback\. Give it default value or assign it in the constructor\.$#' identifier: property.uninitialized @@ -1479,13 +1491,13 @@ parameters: - message: '#^Cannot call method append\(\) on mixed\.$#' identifier: method.nonObject - count: 6 + count: 8 path: src/Resources/config/definition/service_worker.php - message: '#^Cannot call method arrayNode\(\) on mixed\.$#' identifier: method.nonObject - count: 17 + count: 18 path: src/Resources/config/definition/service_worker.php - @@ -1515,7 +1527,7 @@ parameters: - message: '#^Cannot call method canBeEnabled\(\) on mixed\.$#' identifier: method.nonObject - count: 1 + count: 2 path: src/Resources/config/definition/service_worker.php - @@ -1527,7 +1539,7 @@ parameters: - message: '#^Cannot call method children\(\) on mixed\.$#' identifier: method.nonObject - count: 11 + count: 12 path: src/Resources/config/definition/service_worker.php - @@ -1539,7 +1551,7 @@ parameters: - message: '#^Cannot call method defaultNull\(\) on mixed\.$#' identifier: method.nonObject - count: 6 + count: 8 path: src/Resources/config/definition/service_worker.php - @@ -1557,13 +1569,13 @@ parameters: - message: '#^Cannot call method end\(\) on mixed\.$#' identifier: method.nonObject - count: 107 + count: 112 path: src/Resources/config/definition/service_worker.php - message: '#^Cannot call method example\(\) on mixed\.$#' identifier: method.nonObject - count: 33 + count: 36 path: src/Resources/config/definition/service_worker.php - @@ -1587,7 +1599,7 @@ parameters: - message: '#^Cannot call method info\(\) on mixed\.$#' identifier: method.nonObject - count: 68 + count: 71 path: src/Resources/config/definition/service_worker.php - @@ -1605,7 +1617,7 @@ parameters: - message: '#^Cannot call method isRequired\(\) on mixed\.$#' identifier: method.nonObject - count: 5 + count: 6 path: src/Resources/config/definition/service_worker.php - @@ -1617,7 +1629,7 @@ parameters: - message: '#^Cannot call method scalarNode\(\) on mixed\.$#' identifier: method.nonObject - count: 37 + count: 40 path: src/Resources/config/definition/service_worker.php - @@ -1872,6 +1884,54 @@ parameters: count: 1 path: src/ServiceWorkerRule/AppendCacheStrategies.php + - + message: '#^Cannot access property \$cacheName on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 1 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Cannot access property \$enabled on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 1 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Cannot access property \$failureMessage on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 2 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Cannot access property \$progressUrl on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 4 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Cannot access property \$successMessage on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 2 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Cannot access property \$successUrl on SpomkyLabs\\PwaBundle\\Dto\\BackgroundFetch\|null\.$#' + identifier: property.nonObject + count: 4 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Unused SpomkyLabs\\PwaBundle\\ServiceWorkerRule\\BackgroundFetchCache\:\:__construct$#' + identifier: shipmonk.deadMethod + count: 1 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + + - + message: '#^Unused SpomkyLabs\\PwaBundle\\ServiceWorkerRule\\BackgroundFetchCache\:\:getPriority$#' + identifier: shipmonk.deadMethod + count: 1 + path: src/ServiceWorkerRule/BackgroundFetchCache.php + - message: '#^Unused SpomkyLabs\\PwaBundle\\ServiceWorkerRule\\ClearCache\:\:__construct$#' identifier: shipmonk.deadMethod diff --git a/src/Dto/BackgroundFetch.php b/src/Dto/BackgroundFetch.php new file mode 100644 index 0000000..c705192 --- /dev/null +++ b/src/Dto/BackgroundFetch.php @@ -0,0 +1,24 @@ +defaultTrue() ->info('Whether the service worker should use the cache.') ->end() + ->arrayNode('background_fetch') + ->canBeEnabled() + ->children() + ->append(getUrlNode('progress_url', 'The URL of the progress page.')) + ->append(getUrlNode('success_url', 'The URL of the success page.')) + ->scalarNode('success_message') + ->info('The message to display on success. This message is translated.') + ->defaultNull() + ->example(['The download is complete.']) + ->end() + ->scalarNode('failure_message') + ->info('The message to display on success. This message is translated.') + ->defaultNull() + ->example(['The download is complete.']) + ->end() + ->end() + ->end() ->arrayNode('workbox') ->info('The configuration of the workbox.') ->canBeDisabled() diff --git a/src/ServiceWorkerRule/BackgroundFetchCache.php b/src/ServiceWorkerRule/BackgroundFetchCache.php new file mode 100644 index 0000000..4fa9169 --- /dev/null +++ b/src/ServiceWorkerRule/BackgroundFetchCache.php @@ -0,0 +1,101 @@ +serviceWorker->backgroundFetch->enabled) { + return ''; + } + + $declaration = ''; + + if ($this->serviceWorker->backgroundFetch->successUrl !== null) { + $successUrl = $this->router->generate( + $this->serviceWorker->backgroundFetch->successUrl->path, + $this->serviceWorker->backgroundFetch->successUrl->params, + $this->serviceWorker->backgroundFetch->successUrl->pathTypeReference + ); + $declaration .= << { + const bgFetch = event.registration; + if (bgFetch.result !== 'success') { + return; + } + clients.openWindow('{$successUrl}'); +}); + +BACKGROUND_FETCH_CACHE; + } + + if ($this->serviceWorker->backgroundFetch->progressUrl !== null) { + $progressUrl = $this->router->generate( + $this->serviceWorker->backgroundFetch->progressUrl->path, + $this->serviceWorker->backgroundFetch->progressUrl->params, + $this->serviceWorker->backgroundFetch->progressUrl->pathTypeReference + ); + $declaration .= << { + const bgFetch = event.registration; + if (bgFetch.result === 'success') { + return; + } + clients.openWindow('{$progressUrl}'); +}); + +BACKGROUND_FETCH_CACHE; + } + + if ($this->serviceWorker->backgroundFetch->successMessage !== null) { + $successMessage = $this->serviceWorker->backgroundFetch->successMessage; + if ($successMessage !== '' && $successMessage !== null) { + $successMessage = $this->translator->trans($successMessage, [], 'pwa'); + } + $declaration .= << { + event.updateUI({ title: "{$successMessage}" }); +}); + +BACKGROUND_FETCH_CACHE; + } + + if ($this->serviceWorker->backgroundFetch->failureMessage !== null) { + $failureMessage = $this->serviceWorker->backgroundFetch->failureMessage; + if ($failureMessage !== '' && $failureMessage !== null) { + $failureMessage = $this->translator->trans($failureMessage, [], 'pwa'); + } + $declaration .= << { + event.updateUI({ title: "{$failureMessage}" }); +}); + +BACKGROUND_FETCH_CACHE; + } + + return $declaration; + } + + public static function getPriority(): int + { + return 1024; + } +}