diff --git a/assets/package.json b/assets/package.json index 013a877..f52c54d 100644 --- a/assets/package.json +++ b/assets/package.json @@ -12,6 +12,13 @@ "fetch": "eager", "enabled": true }, + "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..4bc4d1c --- /dev/null +++ b/assets/src/backgroundfetch_controller.js @@ -0,0 +1,61 @@ +'use strict'; + +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + download = async ({params}) => { + const { id, requests, title, icons, downloadTotal } = params; + if (!requests) { + return; + } + const processId = id || self.crypto.randomUUID(); + await this.fetch(processId, requests, { + title, + icons, + downloadTotal + }); + } + + fetch = async (id, requests, options) => { + try { + const registration = await navigator.serviceWorker.ready; + const bgFetch = await registration.backgroundFetch.fetch(id, requests, options); + bgFetch.addEventListener('progress', () => this.dispatchStatus(bgFetch)); + + } catch (error) { + this.dispatchEvent('error', { error }); + } + } + + status = async () => { + const registration = await navigator.serviceWorker.ready; + const ids = await registration.backgroundFetch.getIds(); + this.dispatchEvent('ids', ids); + const promises = ids.map(async (id) => { + const bgFetch = await registration.backgroundFetch.get(id); + if (!bgFetch) { + return; + } + bgFetch.addEventListener('progress', () => this.dispatchStatus(bgFetch)); + }); + await Promise.all(promises); + } + + dispatchStatus = (bgFetch) => { + this.dispatchEvent('status', { + id: bgFetch.id, + uploadTotal: bgFetch.uploadTotal, + uploaded: bgFetch.uploaded, + downloadTotal: bgFetch.downloadTotal, + downloaded: bgFetch.downloaded, + result: bgFetch.result, + failureReason: bgFetch.failureReason, + recordsAvailable: bgFetch.recordsAvailable, + }); + } + + dispatchEvent = (name, payload) => { + this.dispatch(name, { detail: payload }); + } +} diff --git a/link b/link old mode 100644 new mode 100755 diff --git a/src/CachingStrategy/AssetCache.php b/src/CachingStrategy/AssetCache.php index 7be0018..7e69c70 100644 --- a/src/CachingStrategy/AssetCache.php +++ b/src/CachingStrategy/AssetCache.php @@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\SerializerInterface; use function count; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/CachingStrategy/ImageCache.php b/src/CachingStrategy/ImageCache.php index daaeffe..506add9 100644 --- a/src/CachingStrategy/ImageCache.php +++ b/src/CachingStrategy/ImageCache.php @@ -11,6 +11,7 @@ use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use function sprintf; final class ImageCache implements HasCacheStrategiesInterface, CanLogInterface { diff --git a/src/CachingStrategy/ManifestCache.php b/src/CachingStrategy/ManifestCache.php index 87dd1dc..712e9d6 100644 --- a/src/CachingStrategy/ManifestCache.php +++ b/src/CachingStrategy/ManifestCache.php @@ -10,6 +10,7 @@ use SpomkyLabs\PwaBundle\Dto\Workbox; use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use function sprintf; final class ManifestCache implements HasCacheStrategiesInterface, CanLogInterface { diff --git a/src/CachingStrategy/PreloadUrlsGeneratorManager.php b/src/CachingStrategy/PreloadUrlsGeneratorManager.php index c9ac49c..b740a19 100644 --- a/src/CachingStrategy/PreloadUrlsGeneratorManager.php +++ b/src/CachingStrategy/PreloadUrlsGeneratorManager.php @@ -10,6 +10,7 @@ use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use function array_key_exists; +use function sprintf; final class PreloadUrlsGeneratorManager implements CanLogInterface { diff --git a/src/CachingStrategy/ResourceCaches.php b/src/CachingStrategy/ResourceCaches.php index 300d803..3fe2e3d 100644 --- a/src/CachingStrategy/ResourceCaches.php +++ b/src/CachingStrategy/ResourceCaches.php @@ -20,6 +20,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\SerializerInterface; use function count; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/CachingStrategy/WorkboxCacheStrategy.php b/src/CachingStrategy/WorkboxCacheStrategy.php index 73076ec..21086f6 100644 --- a/src/CachingStrategy/WorkboxCacheStrategy.php +++ b/src/CachingStrategy/WorkboxCacheStrategy.php @@ -6,6 +6,7 @@ use SpomkyLabs\PwaBundle\WorkboxPlugin\CachePluginInterface; use function in_array; +use function sprintf; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; diff --git a/src/Command/CreateIconsCommand.php b/src/Command/CreateIconsCommand.php index 3d822cd..16d464b 100644 --- a/src/Command/CreateIconsCommand.php +++ b/src/Command/CreateIconsCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Yaml\Yaml; use function is_string; +use function sprintf; #[AsCommand(name: 'pwa:create:icons', description: 'Generate icons for your PWA')] final class CreateIconsCommand extends Command diff --git a/src/Command/CreateScreenshotCommand.php b/src/Command/CreateScreenshotCommand.php index dcd6aa4..abe3e8b 100644 --- a/src/Command/CreateScreenshotCommand.php +++ b/src/Command/CreateScreenshotCommand.php @@ -24,6 +24,7 @@ use function assert; use function is_int; use function is_string; +use function sprintf; #[AsCommand( name: 'pwa:create:screenshot', diff --git a/src/CompilerPass/PreloadUrlCompilerPass.php b/src/CompilerPass/PreloadUrlCompilerPass.php index 286e173..8ecbf9d 100644 --- a/src/CompilerPass/PreloadUrlCompilerPass.php +++ b/src/CompilerPass/PreloadUrlCompilerPass.php @@ -17,6 +17,7 @@ use Throwable; use function array_key_exists; use function is_string; +use function sprintf; /** * @internal diff --git a/src/Dto/BackgroundFetch.php b/src/Dto/BackgroundFetch.php new file mode 100644 index 0000000..449b80e --- /dev/null +++ b/src/Dto/BackgroundFetch.php @@ -0,0 +1,27 @@ +width === $configuration->height) { - $mainImage = imagescale($mainImage, $configuration->width, $configuration->height); - assert($mainImage !== false); - - return $mainImage; - }*/ + * $mainImage = imagescale($mainImage, $configuration->width, $configuration->height); + * assert($mainImage !== false); + * return $mainImage; + * }*/ $srcWidth = imagesx($mainImage); $srcHeight = imagesy($mainImage); diff --git a/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php b/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php index ba4ff12..d99250c 100644 --- a/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/DestinationMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class DestinationMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php b/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php index 050177a..a295b33 100644 --- a/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/ExactPathnameMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class ExactPathnameMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/OriginMatchCallbackHandler.php b/src/MatchCallbackHandler/OriginMatchCallbackHandler.php index 7d0eb7a..f34f943 100644 --- a/src/MatchCallbackHandler/OriginMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/OriginMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class OriginMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php b/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php index f134627..71ed16c 100644 --- a/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/PathnameEndsWithMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class PathnameEndsWithMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php b/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php index b643f86..d0ed63b 100644 --- a/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/PathnameStartsWithMatchCallbackHandler.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; +use function sprintf; final class PathnameStartsWithMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/MatchCallbackHandler/RouteMatchCallbackHandler.php b/src/MatchCallbackHandler/RouteMatchCallbackHandler.php index 2494790..bfc51cf 100644 --- a/src/MatchCallbackHandler/RouteMatchCallbackHandler.php +++ b/src/MatchCallbackHandler/RouteMatchCallbackHandler.php @@ -8,6 +8,7 @@ use Psr\Log\NullLogger; use SpomkyLabs\PwaBundle\Service\CanLogInterface; use Symfony\Component\Routing\RouterInterface; +use function sprintf; final class RouteMatchCallbackHandler implements MatchCallbackHandlerInterface, CanLogInterface { diff --git a/src/Normalizer/ScreenshotNormalizer.php b/src/Normalizer/ScreenshotNormalizer.php index 9efb9c1..f816a1e 100644 --- a/src/Normalizer/ScreenshotNormalizer.php +++ b/src/Normalizer/ScreenshotNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; +use function sprintf; final class ScreenshotNormalizer implements NormalizerInterface, NormalizerAwareInterface { diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index 2e8e769..548f22a 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -42,6 +42,28 @@ ->defaultTrue() ->info('Whether the service worker should use the cache.') ->end() + ->arrayNode('background_fetch') + ->canBeEnabled() + ->children() + ->scalarNode('cache_name') + ->isRequired() + ->info('The name of the cache where files should be saved.') + ->example(['downloads']) + ->end() + ->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 can be translated.') + ->defaultNull() + ->example(['The download is complete.']) + ->end() + ->scalarNode('failure_message') + ->info('The message to display on success. This message can be translated.') + ->defaultNull() + ->example(['The download is complete.']) + ->end() + ->end() + ->end() ->arrayNode('workbox') ->info('The configuration of the workbox.') ->canBeDisabled() diff --git a/src/Service/FaviconsCompiler.php b/src/Service/FaviconsCompiler.php index 50e9fad..37fbb1c 100644 --- a/src/Service/FaviconsCompiler.php +++ b/src/Service/FaviconsCompiler.php @@ -15,6 +15,7 @@ use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use function assert; +use function sprintf; use const PHP_EOL; final class FaviconsCompiler implements FileCompilerInterface, CanLogInterface diff --git a/src/Service/IconResolver.php b/src/Service/IconResolver.php index f13692b..07bf878 100644 --- a/src/Service/IconResolver.php +++ b/src/Service/IconResolver.php @@ -17,6 +17,7 @@ use function assert; use function count; use function is_array; +use function sprintf; final readonly class IconResolver { diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index be25c01..955fdd2 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -17,6 +17,7 @@ use function in_array; use function is_array; use function is_string; +use function sprintf; final class ServiceWorkerCompiler implements FileCompilerInterface, CanLogInterface { diff --git a/src/ServiceWorkerRule/AppendCacheStrategies.php b/src/ServiceWorkerRule/AppendCacheStrategies.php index cda6871..219a578 100644 --- a/src/ServiceWorkerRule/AppendCacheStrategies.php +++ b/src/ServiceWorkerRule/AppendCacheStrategies.php @@ -7,6 +7,7 @@ use SpomkyLabs\PwaBundle\CachingStrategy\HasCacheStrategiesInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; +use function sprintf; use const PHP_EOL; final readonly class AppendCacheStrategies implements ServiceWorkerRuleInterface diff --git a/src/ServiceWorkerRule/BackgroundFetchCache.php b/src/ServiceWorkerRule/BackgroundFetchCache.php new file mode 100644 index 0000000..94d02b8 --- /dev/null +++ b/src/ServiceWorkerRule/BackgroundFetchCache.php @@ -0,0 +1,122 @@ +serviceWorker->backgroundFetch->enabled) { + return ''; + } + + $declaration = << { + event.waitUntil( + (async () => { + try { + const cache = await caches.open('{$this->serviceWorker->backgroundFetch->cacheName}'); + const records = await event.registration.matchAll(); + const promises = records.map(async record => { + const response = await record.responseReady; + await cache.put(record.request, response); + }); + await Promise.all(promises); + } catch (err) { + // Do nothing + } + })() + ); +}); + +BACKGROUND_FETCH_CACHE; + + 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; + } +} diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index 32cd7e2..484a0aa 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\MimeTypes; use function array_key_exists; +use function sprintf; use const ENT_COMPAT; use const ENT_SUBSTITUTE; use const PHP_EOL; diff --git a/src/WorkboxPlugin/BroadcastUpdatePlugin.php b/src/WorkboxPlugin/BroadcastUpdatePlugin.php index 8d5011d..33fde5a 100644 --- a/src/WorkboxPlugin/BroadcastUpdatePlugin.php +++ b/src/WorkboxPlugin/BroadcastUpdatePlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class BroadcastUpdatePlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'BroadcastUpdatePlugin'; diff --git a/src/WorkboxPlugin/CacheableResponsePlugin.php b/src/WorkboxPlugin/CacheableResponsePlugin.php index 011d145..6cec135 100644 --- a/src/WorkboxPlugin/CacheableResponsePlugin.php +++ b/src/WorkboxPlugin/CacheableResponsePlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class CacheableResponsePlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'CacheableResponsePlugin'; diff --git a/src/WorkboxPlugin/ExpirationPlugin.php b/src/WorkboxPlugin/ExpirationPlugin.php index 79eb733..141b913 100644 --- a/src/WorkboxPlugin/ExpirationPlugin.php +++ b/src/WorkboxPlugin/ExpirationPlugin.php @@ -4,6 +4,8 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function sprintf; + final readonly class ExpirationPlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'ExpirationPlugin'; diff --git a/tests/Functional/AbstractPwaTestCase.php b/tests/Functional/AbstractPwaTestCase.php index 64a27b5..046c286 100644 --- a/tests/Functional/AbstractPwaTestCase.php +++ b/tests/Functional/AbstractPwaTestCase.php @@ -8,6 +8,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; use function assert; +use function sprintf; /** * @internal diff --git a/tests/Functional/GenerateIconsCommandTest.php b/tests/Functional/GenerateIconsCommandTest.php index 77b16c6..7e2b2ee 100644 --- a/tests/Functional/GenerateIconsCommandTest.php +++ b/tests/Functional/GenerateIconsCommandTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; use function assert; +use function sprintf; /** * @internal diff --git a/tests/Functional/TakeScreenshotCommandTest.php b/tests/Functional/TakeScreenshotCommandTest.php index accd081..a3e2592 100644 --- a/tests/Functional/TakeScreenshotCommandTest.php +++ b/tests/Functional/TakeScreenshotCommandTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; use function assert; +use function sprintf; /** * @internal diff --git a/tests/TestFilesystem.php b/tests/TestFilesystem.php index 002a7b5..70a7e02 100644 --- a/tests/TestFilesystem.php +++ b/tests/TestFilesystem.php @@ -7,6 +7,7 @@ use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use function dirname; +use function sprintf; final readonly class TestFilesystem implements PublicAssetsFilesystemInterface {