diff --git a/frontend/controller/service-worker.js b/frontend/controller/service-worker.js index 574fa4d42..8d73ff37c 100644 --- a/frontend/controller/service-worker.js +++ b/frontend/controller/service-worker.js @@ -51,6 +51,34 @@ sbp('sbp/selectors/register', { await swRegistration.update() setInterval(() => sbp('service-worker/update'), HOURS_MILLIS) + // Send a 'ready' message to the SW and wait back for a response + // This way we ensure that Chelonia has been set up + await new Promise((resolve, reject) => { + const messageChannel = new MessageChannel() + messageChannel.port1.onmessage = (event) => { + if (event.data.type === 'ready') { + resolve() + } else { + reject(event.data.error) + } + messageChannel.port1.close() + } + messageChannel.port1.onmessageerror = () => { + reject(new Error('Message error')) + messageChannel.port1.close() + } + + navigator.serviceWorker.ready.then((worker) => { + worker.active.postMessage({ + type: 'ready', + port: messageChannel.port2 + }, [messageChannel.port2]) + }).catch((e) => { + reject(e) + messageChannel.port1.close() + }) + }) + // Keep the service worker alive while the window is open // The default idle timeout on Chrome and Firefox is 30 seconds. We send // a ping message every 5 seconds to ensure that the worker remains @@ -92,6 +120,7 @@ sbp('sbp/selectors/register', { }) } catch (e) { console.error('error setting up service worker:', e) + throw e } }, // We call this when the notification permission changes, to create a push diff --git a/frontend/controller/serviceworkers/push.js b/frontend/controller/serviceworkers/push.js index 0af7c66c1..d6eea834a 100644 --- a/frontend/controller/serviceworkers/push.js +++ b/frontend/controller/serviceworkers/push.js @@ -30,7 +30,7 @@ export default (sbp('sbp/selectors/register', { const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE) if (!pubsub) reject(new Error('Missing pubsub instance')) - const readyState = pubsub.socket.readyState + const readyState = pubsub.socket?.readyState if (readyState !== WebSocket.OPEN) { reject(new Error('WebSocket connection is not open')) } @@ -71,7 +71,7 @@ export default (sbp('sbp/selectors/register', { const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE) if (!pubsub) throw new Error('Missing pubsub instance') - const readyState = pubsub.socket.readyState + const readyState = pubsub.socket?.readyState if (readyState !== WebSocket.OPEN) { throw new Error('WebSocket connection is not open') } diff --git a/frontend/controller/serviceworkers/sw-primary.js b/frontend/controller/serviceworkers/sw-primary.js index 3c3f9b83d..c4e3e7910 100644 --- a/frontend/controller/serviceworkers/sw-primary.js +++ b/frontend/controller/serviceworkers/sw-primary.js @@ -244,6 +244,26 @@ self.addEventListener('message', function (event) { case 'event': sbp('okTurtles.events/emit', event.data.subtype, ...deserializer(event.data.data)) break + case 'ready': { + // The 'ready' message is sent by a client (i.e., a tab or window) to + // ensure that Chelonia has been setup + const port = event.data.port + Promise.race([ + setupChelonia(), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('Timed out setting up Chelonia')) + }, 30e3) + }) + ]).then(() => { + port.postMessage({ type: 'ready' }) + }, (e) => { + port.postMessage({ type: 'error', error: e }) + }).finally(() => { + port.close() + }) + break + } default: console.error('[sw] unknown message type:', event.data) break diff --git a/frontend/main.js b/frontend/main.js index cd9306bef..2acc28f83 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -152,7 +152,7 @@ async function startApp () { new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('Timed out setting up service worker')) - }, 16e3) + }, 8e3) })] ).catch(e => { console.error('[main] Error setting up service worker', e) diff --git a/frontend/setupChelonia.js b/frontend/setupChelonia.js index 625c3880f..dde4dab1e 100644 --- a/frontend/setupChelonia.js +++ b/frontend/setupChelonia.js @@ -295,9 +295,12 @@ const setupChelonia = async (): Promise<*> => { if (cheloniaState.loggedIn?.identityContractID !== identityContractID) return // it is important we first login before syncing any contracts here since that will load the // state and the contract sideEffects will sometimes need that state, e.g. loggedIn.identityContractID - await sbp('chelonia/contract/sync', identityContractID) - const contractIDs = groupContractsByType(cheloniaState.contracts) - await syncContractsInOrder(contractIDs) + await sbp('chelonia/contract/sync', identityContractID).then(async () => { + const contractIDs = groupContractsByType(cheloniaState.contracts) + await syncContractsInOrder(contractIDs) + }).catch(e => { + console.error('[setupChelonia] Error syncing identity contract and groups', e) + }) }) } @@ -310,6 +313,7 @@ export default ((() => { return () => { if (!promise) { promise = setupChelonia().catch((e) => { + console.error('[setupChelonia] Error during chelonia setup', e) promise = undefined // Reset on error throw e // Re-throw the error })