diff --git a/assets/package.json b/assets/package.json index 0eb456e..16e0ec2 100644 --- a/assets/package.json +++ b/assets/package.json @@ -18,6 +18,13 @@ "webpackMode": "eager", "fetch": "eager", "enabled": true + }, + "sync-broadcast": { + "main": "src/sync-broadcast_controller.js", + "name": "pwa/sync-broadcast", + "webpackMode": "eager", + "fetch": "eager", + "enabled": true } }, "importmap": { diff --git a/assets/src/backgroundsync-form_controller.js b/assets/src/backgroundsync-form_controller.js index e357e8c..02a9874 100644 --- a/assets/src/backgroundsync-form_controller.js +++ b/assets/src/backgroundsync-form_controller.js @@ -16,7 +16,7 @@ export default class extends Controller { redirection: { type: String, default: null }, }; - async send(event) { + send = async (event) => { event.preventDefault(); const form = this.element; if (!form instanceof HTMLFormElement || !form.checkValidity()) { @@ -45,15 +45,13 @@ export default class extends Controller { window.location.assign(response.url); return; } - if (redirectTo) { + if (redirectTo !== undefined) { window.location.assign(redirectTo); } } catch (error) { - if (redirectTo) { + if (redirectTo !== undefined) { window.location.assign(redirectTo); } - } finally { - form.reset(); } } } diff --git a/assets/src/connection-status_controller.js b/assets/src/connection-status_controller.js index 331827e..ac191b4 100644 --- a/assets/src/connection-status_controller.js +++ b/assets/src/connection-status_controller.js @@ -10,7 +10,7 @@ export default class extends Controller { offlineMessage: { type: String, default: 'You are offline.' }, }; - connect() { + connect = () => { this.dispatchEvent('connect', {}); if (navigator.onLine) { this.statusChanged({ @@ -37,11 +37,11 @@ export default class extends Controller { }); }); } - dispatchEvent(name, payload) { + dispatchEvent = (name, payload) => { this.dispatch(name, { detail: payload, prefix: 'connection-status' }); } - statusChanged(data) { + statusChanged = (data) => { this.messageTargets.forEach((element) => { element.innerHTML = data.message; }); diff --git a/assets/src/sync-broadcast_controller.js b/assets/src/sync-broadcast_controller.js new file mode 100644 index 0000000..f8ea1a4 --- /dev/null +++ b/assets/src/sync-broadcast_controller.js @@ -0,0 +1,39 @@ +'use strict'; + +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static values = { + channel: { type: String }, + }; + static targets = ['remaining']; + + bc = null; + + connect = () => { + if (!this.channelValue) { + throw new Error('The channel value is required.'); + } + this.bc = new BroadcastChannel(this.channelValue); + this.bc.onmessage = this.messageReceived; + } + + disconnect = () => { + if (this.bc !== null) { + this.bc.close(); + } + } + + dispatchEvent = (name, payload) => { + this.dispatch(name, { detail: payload, prefix: 'connection-status' }); + } + + messageReceived = async (event) => { + const data = event.data; + this.remainingTargets.forEach((element) => { + element.innerHTML = data.remaining; + }); + this.dispatchEvent('status-changed', { detail: data }); + } +} diff --git a/src/Dto/BackgroundSync.php b/src/Dto/BackgroundSync.php index 7d34b71..2844f1c 100644 --- a/src/Dto/BackgroundSync.php +++ b/src/Dto/BackgroundSync.php @@ -18,6 +18,9 @@ final class BackgroundSync #[SerializedName('max_retention_time')] public int $maxRetentionTime; - #[SerializedName('force_sync_callback')] + #[SerializedName('force_sync_fallback')] public bool $forceSyncFallback; + + #[SerializedName('broadcast_channel')] + public null|string $broadcastChannel = null; } diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index abbb1fa..cdcf490 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -342,13 +342,20 @@ ->info('The HTTP method.') ->example(['POST', 'PUT', 'PATCH', 'DELETE']) ->end() + ->scalarNode('broadcast_channel') + ->defaultNull() + ->info('The broadcast channel. Set null to disable.') + ->example(['channel-1', 'background-sync-events']) + ->end() ->integerNode('max_retention_time') - ->defaultValue(60 * 24 * 5) + ->defaultValue(60 * 24) ->info('The maximum retention time in minutes.') ->end() - ->booleanNode('force_sync_callback') + ->booleanNode('force_sync_fallback') ->defaultFalse() - ->info('Whether to force the sync callback.') + ->info( + 'If `true`, instead of attempting to use background sync events, always attempt to replay queued request at service worker startup. Most folks will not need this, unless you explicitly target a runtime like Electron that exposes the interfaces for background sync, but does not have a working implementation.' + ) ->end() ->end() ->end() diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index 5704ece..66d7697 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -332,16 +332,32 @@ private function processBackgroundSyncRule(Workbox $workbox, string $body): stri $declaration = ''; foreach ($workbox->backgroundSync as $sync) { - $options = [ - 'maxRetentionTime' => $sync->maxRetentionTime, - 'forceSyncCallback' => $sync->forceSyncFallback, - ]; - $options = array_filter($options, static fn (mixed $v): bool => $v !== null); - $options = count($options) === 0 ? '' : $this->serializer->serialize($options, 'json', $this->jsonOptions); + $forceSyncFallback = $sync->forceSyncFallback === true ? 'true' : 'false'; + $broadcastChannel = ''; + if ($sync->broadcastChannel !== null) { + $broadcastChannel = << { + try { + await queue.replayRequests(); + } catch (error) { + // Failed to replay one or more requests + } finally { + remainingRequests = await queue.getAll(); + const bc = new BroadcastChannel('{$sync->broadcastChannel}'); + bc.postMessage({name: '{$sync->queueName}', remaining: remainingRequests.length}); + bc.close(); + } + } +BROADCAST_CHANNEL; + } $declaration .= <<regex}'), - new workbox.strategies.NetworkOnly({plugins: [new workbox.backgroundSync.BackgroundSyncPlugin('{$sync->queueName}',{$options})] }), + new workbox.strategies.NetworkOnly({plugins: [new workbox.backgroundSync.BackgroundSyncPlugin('{$sync->queueName}',{ + "maxRetentionTime": {$sync->maxRetentionTime}, + "forceSyncFallback": {$forceSyncFallback}{$broadcastChannel} +})] }), '{$sync->method}' ); BACKGROUND_SYNC_RULE_STRATEGY;