diff --git a/assets/src/backgroundsync-form_controller.js b/assets/src/backgroundsync-form_controller.js index ac47b58..28a9238 100644 --- a/assets/src/backgroundsync-form_controller.js +++ b/assets/src/backgroundsync-form_controller.js @@ -31,16 +31,22 @@ export default class extends Controller { try { const params = this.paramsValue; params.headers = this.headersValue; - if (form.enctype === 'multipart/form-data') { - params.body = new FormData(form); - } else if (form.enctype === 'application/json') { - params.body = JSON.stringify(Object.fromEntries(new FormData(form))); - } else if (form.enctype === 'application/x-www-form-urlencoded') { - params.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - params.body = new URLSearchParams(new FormData(form)); - } else { - // Unsupported form enctype - return; + switch (form.enctype) { + case 'multipart/form-data': + params.headers['Content-Type'] = 'multipart/form-data'; + params.body = new FormData(form); + break; + case 'application/json': + params.headers['Content-Type'] = 'application/json'; + params.body = JSON.stringify(Object.fromEntries(new FormData(form))); + break; + case 'application/x-www-form-urlencoded': + params.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + params.body = (new URLSearchParams(new FormData(form))).toString(); + break; + default: + console.error('Unknown form enctype'); + return; } params.method = form.method.toUpperCase(); const response = await fetch(url, params); diff --git a/src/CachingStrategy/BackgroundSync.php b/src/CachingStrategy/BackgroundSync.php index a62ae84..40bf521 100644 --- a/src/CachingStrategy/BackgroundSync.php +++ b/src/CachingStrategy/BackgroundSync.php @@ -51,7 +51,11 @@ public function getCacheStrategies(): array $sync->queueName, $sync->maxRetentionTime, $sync->forceSyncFallback, - $sync->broadcastChannel + $sync->broadcastChannel, + $sync->errorOn4xx, + $sync->errorOn5xx, + $sync->expectRedirect, + $sync->expectedStatusCodes, ), ) ->withMethod($sync->method); diff --git a/src/Dto/BackgroundSync.php b/src/Dto/BackgroundSync.php index 0944a93..0451a50 100644 --- a/src/Dto/BackgroundSync.php +++ b/src/Dto/BackgroundSync.php @@ -14,6 +14,21 @@ final class BackgroundSync extends Cache #[SerializedName('match_callback')] public string $matchCallback; + #[SerializedName('error_on_4xx')] + public bool $errorOn4xx = false; + + #[SerializedName('error_on_5xx')] + public bool $errorOn5xx = true; + + #[SerializedName('expect_redirect')] + public bool $expectRedirect = false; + + /** + * @var array + */ + #[SerializedName('expected_status_codes')] + public array $expectedStatusCodes = []; + public string $method; #[SerializedName('max_retention_time')] diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index 2e8e769..b28e3e5 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -393,6 +393,7 @@ ->treatTrueLike([]) ->info('The background sync configuration.') ->arrayPrototype() + ->addDefaultsIfNotSet() ->children() ->scalarNode('queue_name') ->isRequired() @@ -404,6 +405,29 @@ ->info('The regex or callback function to match the URLs.') ->example(['/\/api\//']) ->end() + ->booleanNode('error_on_4xx') + ->defaultTrue() + ->info('Whether to retry the request on 4xx errors.') + ->end() + ->booleanNode('error_on_5xx') + ->defaultTrue() + ->info('Whether to retry the request on 5xx errors.') + ->end() + ->arrayNode('expected_status_codes') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->info( + 'The expected success status codes. If the response status code is not in the list, the request will be retried.' + ) + ->integerPrototype()->end() + ->end() + ->booleanNode('expect_redirect') + ->defaultFalse() + ->info( + 'Whether to expect a redirect (JS response type should be "opaqueredirect" or the "redirected" property is "true").' + ) + ->end() ->scalarNode('method') ->defaultValue('POST') ->info('The HTTP method.') diff --git a/src/WorkboxPlugin/BackgroundSyncPlugin.php b/src/WorkboxPlugin/BackgroundSyncPlugin.php index c1c9bea..f34b7ab 100644 --- a/src/WorkboxPlugin/BackgroundSyncPlugin.php +++ b/src/WorkboxPlugin/BackgroundSyncPlugin.php @@ -4,15 +4,24 @@ namespace SpomkyLabs\PwaBundle\WorkboxPlugin; +use function count; + final readonly class BackgroundSyncPlugin implements CachePluginInterface, HasDebugInterface { private const NAME = 'BackgroundSyncPlugin'; + /** + * @param array $expectedStatusCodes + */ public function __construct( public string $queueName, public bool $forceSyncFallback, public null|string $broadcastChannel, public int $maxRetentionTime, + public bool $errorOn4xx = false, + public bool $errorOn5xx = true, + public bool $expectRedirect = false, + public array $expectedStatusCodes = [], ) { } @@ -42,23 +51,43 @@ public function render(int $jsonOptions = 0): string BROADCAST_CHANNEL; } + $errorOn4xx = $this->getErrorOn4xx(); + $errorOn5xx = $this->getErrorOn5xx(); + $expectRedirect = $this->getExpectRedirect(); + $expectedStatusCodes = $this->getExpectedSuccessStatusCodes(); + $declaration = <<queueName}',{ - "maxRetentionTime": {$this->maxRetentionTime}, - "forceSyncFallback": {$forceSyncFallback}{$broadcastChannelSection} -}) + + {$errorOn4xx}{$errorOn5xx}{$expectRedirect}{$expectedStatusCodes}new workbox.backgroundSync.BackgroundSyncPlugin('{$this->queueName}',{"maxRetentionTime": {$this->maxRetentionTime}, "forceSyncFallback": {$forceSyncFallback}{$broadcastChannelSection}}) + BACKGROUND_SYNC_RULE_STRATEGY; return trim($declaration); } + /** + * @param array $expectedStatusCodes + */ public static function create( string $queueName, int $maxRetentionTime, bool $forceSyncFallback, - null|string $broadcastChannel + null|string $broadcastChannel, + bool $errorOn4xx = false, + bool $errorOn5xx = true, + bool $expectRedirect = false, + array $expectedStatusCodes = [], ): static { - return new self($queueName, $forceSyncFallback, $broadcastChannel, $maxRetentionTime); + return new self( + $queueName, + $forceSyncFallback, + $broadcastChannel, + $maxRetentionTime, + $errorOn4xx, + $errorOn5xx, + $expectRedirect, + $expectedStatusCodes, + ); } public function getDebug(): array @@ -68,6 +97,52 @@ public function getDebug(): array 'forceSyncFallback' => $this->forceSyncFallback, 'broadcastChannel' => $this->broadcastChannel, 'maxRetentionTime' => $this->maxRetentionTime, + 'errorOn4xx' => $this->errorOn4xx, + 'errorOn5xx' => $this->errorOn5xx, + 'expectRedirect' => $this->expectRedirect, + 'expectedSuccessStatusCodes' => $this->expectedStatusCodes, ]; } + + private function getErrorOn4xx(): string + { + if ($this->errorOn5xx === false) { + return ''; + } + + return $this->getErrorOn(400); + } + + private function getErrorOn5xx(): string + { + if ($this->errorOn5xx === false) { + return ''; + } + + return $this->getErrorOn(500); + } + + private function getErrorOn(int $statusCode): string + { + return "{fetchDidSucceed: ({response}) => {if (response.status >= {$statusCode}) {throw new Error('Server error.');}return response;}},"; + } + + private function getExpectedSuccessStatusCodes(): string + { + if (count($this->expectedStatusCodes) === 0) { + return ''; + } + $codes = implode(',', $this->expectedStatusCodes); + + return "{fetchDidSucceed: ({response}) => {if (! [{$codes}].includes(response.status)) {throw new Error('Unexpected response status code. Expected one of [{$codes}]. Got ' + response.status);}return response;}},"; + } + + private function getExpectRedirect(): string + { + if ($this->expectRedirect === false) { + return ''; + } + + return "{fetchDidSucceed: ({response}) => {if (response.type !== 'opaqueredirect' || response.redirect !== true) {throw new Error('Expected a redirect response.');}return response;}},"; + } }