Skip to content

Commit

Permalink
Chelonia in SW
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Sep 19, 2024
1 parent e03e16b commit 45eccd8
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 37 deletions.
24 changes: 3 additions & 21 deletions frontend/controller/namespace.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
'use strict'

import sbp from '@sbp/sbp'
import Vue from 'vue'

// NOTE: prefix groups with `group/` and users with `user/` ?
sbp('sbp/selectors/register', {
'namespace/lookupCached': (name: string) => {
const cache = sbp('state/vuex/state').namespaceLookups
return cache[name] ?? null
return cache?.[name] ?? null
},
'namespace/lookupReverseCached': (id: string) => {
const cache = sbp('state/vuex/state').reverseNamespaceLookups
return cache[id] ?? null
return cache?.[id] ?? null
},
'namespace/lookup': (name: string, { skipCache }: { skipCache: boolean } = { skipCache: false }) => {
if (!skipCache) {
Expand All @@ -23,23 +22,6 @@ sbp('sbp/selectors/register', {
return Promise.resolve(cached)
}
}
return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${encodeURIComponent(name)}`).then((r: Object) => {
if (!r.ok) {
console.warn(`namespace/lookup: ${r.status} for ${name}`)
if (r.status !== 404) {
throw new Error(`${r.status}: ${r.statusText}`)
}
return null
}
return r['text']()
}).then(value => {
if (value !== null) {
const cache = sbp('state/vuex/state').namespaceLookups
const reverseCache = sbp('state/vuex/state').reverseNamespaceLookups
Vue.set(cache, name, value)
Vue.set(reverseCache, value, name)
}
return value
})
return sbp('sw-namespace/lookup', name, { skipCache })
}
})
12 changes: 9 additions & 3 deletions frontend/controller/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict'

import sbp from '@sbp/sbp'
import { PUBSUB_INSTANCE } from '@controller/instance-keys.js'
import { REQUEST_TYPE, PUSH_SERVER_ACTION_TYPE, PUBSUB_RECONNECTION_SUCCEEDED, createMessage } from '~/shared/pubsub.js'
import { HOURS_MILLIS } from '~/frontend/model/contracts/shared/time.js'
import sbp from '@sbp/sbp'
import { PWA_INSTALLABLE } from '@utils/events.js'
import { HOURS_MILLIS } from '~/frontend/model/contracts/shared/time.js'
import { PUBSUB_RECONNECTION_SUCCEEDED, PUSH_SERVER_ACTION_TYPE, REQUEST_TYPE, createMessage } from '~/shared/pubsub.js'
import { deserializer } from '~/shared/serdes/index.js'

const pwa = {
deferredInstallPrompt: null,
Expand Down Expand Up @@ -66,6 +67,11 @@ sbp('sbp/selectors/register', {
sbp('service-worker/resubscribe-push', data.subscription)
break
}
case 'event': {
console.error('@@@EVENT RECEIVED', event.data.subtype, ...deserializer(event.data.data))
sbp('okTurtles.events/emit', event.data.subtype, ...deserializer(event.data.data))
break
}
default:
console.error('[sw] Received unknown message type from the service worker:', data)
break
Expand Down
69 changes: 68 additions & 1 deletion frontend/controller/serviceworkers/sw-primary.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
'use strict'

import { PROPOSAL_ARCHIVED } from '@model/contracts/shared/constants.js'
import '@sbp/okturtles.data'
import '@sbp/okturtles.eventqueue'
import '@sbp/okturtles.events'
import sbp from '@sbp/sbp'
import '~/frontend/controller/actions/index.js'
import setupChelonia from '~/frontend/setupChelonia.js'
import { LOGIN, LOGIN_ERROR, LOGOUT } from '~/frontend/utils/events.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED } from '~/shared/domains/chelonia/events.js'
import { deserializer, serializer } from '~/shared/serdes/index.js'
import { ACCEPTED_GROUP, DELETED_CHATROOM, JOINED_CHATROOM, JOINED_GROUP, LEFT_CHATROOM, LEFT_GROUP, NAMESPACE_REGISTRATION, SWITCH_GROUP } from '../../utils/events.js'
import '~/frontend/controller/sw-namespace.js'

deserializer.register(GIMessage)
deserializer.register(Secret)

// https://serviceworke.rs/message-relay_service-worker_doc.html
// https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
// https://jakearchibald.com/2014/using-serviceworker-today/
// https://github.com/w3c/ServiceWorker/blob/master/explainer.md
// https://frontendian.co/service-workers
// https://stackoverflow.com/a/49748437 => https://medium.com/@nekrtemplar/self-destroying-serviceworker-73d62921d717 => https://love2dev.com/blog/how-to-uninstall-a-service-worker/

sbp('sbp/filters/global/add', (domain, selector, data) => {
// if (domainBlacklist[domain] || selectorBlacklist[selector]) return
console.debug(`[sw] [sbp] ${selector}`, data)
});

[EVENT_HANDLED, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, LOGIN, LOGIN_ERROR, LOGOUT, ACCEPTED_GROUP, DELETED_CHATROOM, LEFT_CHATROOM, LEFT_GROUP, JOINED_CHATROOM, JOINED_GROUP, NAMESPACE_REGISTRATION, SWITCH_GROUP, PROPOSAL_ARCHIVED].forEach(et => {
sbp('okTurtles.events/on', et, (...args) => {
const { data } = serializer(args)
const message = {
type: 'event',
subtype: et,
data
}
self.clients.matchAll()
.then((clientList) => {
clientList.forEach((client) => {
client.postMessage(message)
})
})
})
})

sbp('sbp/selectors/register', {
'state/vuex/state': () => {
// TODO: Remove this selector once it's removed from contracts
return sbp('chelonia/rootState')
},
'state/vuex/reset': () => {
console.error('[sw] CALLED state/vuex/reset WHICH IS UNDEFINED')
},
'state/vuex/save': () => {
console.error('[sw] CALLED state/vuex/save WHICH IS UNDEFINED')
},
'state/vuex/commit': () => {
console.error('[sw] CALLED state/vuex/commit WHICH IS UNDEFINED')
}
})

self.addEventListener('install', function (event) {
console.debug('[sw] install')
event.waitUntil(self.skipWaiting())
Expand All @@ -16,7 +72,7 @@ self.addEventListener('activate', function (event) {
console.debug('[sw] activate')

// 'clients.claim()' reference: https://web.dev/articles/service-worker-lifecycle#clientsclaim
event.waitUntil(self.clients.claim())
event.waitUntil(setupChelonia().then(() => self.clients.claim()))
})

self.addEventListener('fetch', function (event) {
Expand Down Expand Up @@ -57,6 +113,17 @@ self.addEventListener('message', function (event) {
case 'store-client-id':
store.clientId = event.source.id
break
case 'sbp': {
const port = event.data.port;
(async () => await sbp(...deserializer(event.data.data)))().then((r) => {
const { data, transferables } = serializer(r)
port.postMessage([true, data], transferables)
}).catch((e) => {
const { data, transferables } = serializer(e)
port.postMessage([false, data], transferables)
})
break
}
case 'ping':
event.source.postMessage({ type: 'pong' })
break
Expand Down
51 changes: 51 additions & 0 deletions frontend/controller/sw-namespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict'

import sbp from '@sbp/sbp'
import { NAMESPACE_REGISTRATION } from '../utils/events.js'

// NOTE: prefix groups with `group/` and users with `user/` ?
sbp('sbp/selectors/register', {
'namespace/lookupCached': (name: string) => {
const cache = sbp('chelonia/rootState').namespaceLookups
// 'cache' may be undefined when starting up or after calling chelonia/reset
return cache?.[name] ?? null
},
'namespace/lookupReverseCached': (id: string) => {
const cache = sbp('chelonia/rootState').reverseNamespaceLookups
return cache?.[id] ?? null
},
'namespace/lookup': (name: string, { skipCache }: { skipCache: boolean } = { skipCache: false }) => {
if (!skipCache) {
const cached = sbp('namespace/lookupCached', name)
if (cached) {
// Wrapping in a Promise to return a consistent type across all execution
// paths (next return is a Promise)
// This way we can call .then() on the result
return Promise.resolve(cached)
}
}
return fetch(`${sbp('okTurtles.data/get', 'API_URL')}/name/${encodeURIComponent(name)}`).then((r: Object) => {
if (!r.ok) {
console.warn(`namespace/lookup: ${r.status} for ${name}`)
if (r.status !== 404) {
throw new Error(`${r.status}: ${r.statusText}`)
}
return null
}
return r['text']()
}).then(value => {
if (value !== null) {
const reactiveSet = sbp('chelonia/config').reactiveSet
const rootState = sbp('chelonia/rootState')
if (!rootState.namespaceLookups) reactiveSet(rootState, 'namespaceLookups', Object.create(null))
if (!rootState.reverseNamespaceLookups) reactiveSet(rootState, 'reverseNamespaceLookups', Object.create(null))
const cache = rootState.namespaceLookups
const reverseCache = rootState.reverseNamespaceLookups
reactiveSet(cache, name, value)
reactiveSet(reverseCache, value, name)
sbp('okTurtles.events/emit', NAMESPACE_REGISTRATION, { name, value })
}
return value
})
}
})
68 changes: 61 additions & 7 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,18 @@ import ALLOWED_URLS from '@view-utils/allowedUrls.js'
import IdleVue from 'idle-vue'
import { mapGetters, mapMutations, mapState } from 'vuex'
import 'wicg-inert'
import '~/shared/domains/chelonia/chelonia.js'
import { CONTRACT_IS_SYNCING } from '~/shared/domains/chelonia/events.js'
import '~/shared/domains/chelonia/localSelectors.js'
import { KV_KEYS } from './utils/constants.js'
// import '~/shared/domains/chelonia/persistent-actions.js' // Commented out as persistentActions are not being used
import './controller/actions/index.js'
import './controller/app/index.js'
import './controller/backend.js'
import './controller/namespace.js'
import router from './controller/router.js'
import './controller/service-worker.js'
import { SETTING_CURRENT_USER } from './model/database.js'
import store from './model/state.js'
import { KV_EVENT, LOGIN_COMPLETE, LOGIN_ERROR, LOGOUT, OFFLINE, ONLINE, RECONNECTING, RECONNECTION_FAILED, SWITCH_GROUP, THEME_CHANGE } from './utils/events.js'
import { KV_EVENT, LOGIN_COMPLETE, LOGIN_ERROR, LOGOUT, NAMESPACE_REGISTRATION, OFFLINE, ONLINE, RECONNECTING, RECONNECTION_FAILED, SWITCH_GROUP, THEME_CHANGE } from './utils/events.js'
import AppStyles from './views/components/AppStyles.vue'
import BannerGeneral from './views/components/banners/BannerGeneral.vue'
import Modal from './views/components/modal/Modal.vue'
Expand All @@ -35,14 +33,19 @@ import './views/utils/ui.js'
import './views/utils/vError.js'
import './views/utils/vFocus.js'
// import './views/utils/vSafeHtml.js' // this gets imported by translations, which is part of common.js
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { deserializer, serializer } from '~/shared/serdes/index.js'
import notificationsMixin from './model/notifications/mainNotificationsMixin.js'
import './model/notifications/periodicNotifications.js'
import setupChelonia from './setupChelonia.js'
import FaviconBadge from './utils/faviconBadge.js'
import './utils/touchInteractions.js'
import { showNavMixin } from './views/utils/misc.js'
import './views/utils/vStyle.js'

deserializer.register(GIMessage)
deserializer.register(Secret)

const { Vue, L, LError } = Common

console.info('GI_VERSION:', process.env.GI_VERSION)
Expand Down Expand Up @@ -99,10 +102,12 @@ async function startApp () {
// TODO: [SW] The following will be needed to keep namespace registrations
// in sync between the SW and each tab. It is not needed now because everything
// is running in the same context
/* sbp('okTurtles.events/on', NAMESPACE_REGISTRATION, ({ name, value }) => {
sbp('okTurtles.events/on', NAMESPACE_REGISTRATION, ({ name, value }) => {
const cache = sbp('state/vuex/state').namespaceLookups
const reverseCache = sbp('state/vuex/state').reverseNamespaceLookups
Vue.set(cache, name, value)
}) */
Vue.set(reverseCache, value, name)
})

// NOTE: setting 'EXPOSE_SBP' in production will make it easier for users to generate contract
// actions that they shouldn't be generating, which can lead to bugs or trigger the automated
Expand Down Expand Up @@ -154,6 +159,55 @@ async function startApp () {
throw e
})

/* TODO: MOVE TO ANOTHER FILE */
sbp('okTurtles.data/set', 'API_URL', self.location.origin)
const swRpc = (() => {
let controller = navigator.serviceWorker.controller
navigator.serviceWorker.addEventListener('controllerchange', (ev) => {
controller = navigator.serviceWorker.controller
}, false)

return (...args) => {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel()
messageChannel.port1.addEventListener('message', (event) => {
if (event.data && Array.isArray(event.data)) {
const r = deserializer(event.data[1])
if (event.data[0] === true) {
resolve(r)
} else {
reject(r)
}
messageChannel.port1.close()
}
}, false)
messageChannel.port1.addEventListener('messageerror', (event) => {
reject(event.data)
messageChannel.port1.close()
}, false)
messageChannel.port1.start()
const { data, transferables } = serializer(args)
controller.postMessage({
type: 'sbp',
port: messageChannel.port2,
data
}, [messageChannel.port2, ...transferables])
})
}
})()

sbp('sbp/selectors/register', {
'gi.actions/*': swRpc
})
sbp('sbp/selectors/register', {
'chelonia/*': swRpc
})
sbp('sbp/selectors/register', {
'sw-namespace/*': (...args) => {
return swRpc(args[0].slice(3), ...args.slice(1))
}
})

/* eslint-disable no-new */
new Vue({
router: router,
Expand Down Expand Up @@ -298,7 +352,7 @@ async function startApp () {
// happened (an example where things can happen this quickly is in the
// tests).
let oldIdentityContractID = null
setupChelonia().then(() => sbp('gi.db/settings/load', SETTING_CURRENT_USER)).then(async (identityContractID) => {
Promise.resolve().then(() => sbp('gi.db/settings/load', SETTING_CURRENT_USER)).then(async (identityContractID) => {
oldIdentityContractID = identityContractID
if (!identityContractID || this.ephemeral.finishedLogin === 'yes') return
await sbp('gi.app/identity/login', { identityContractID })
Expand Down
5 changes: 3 additions & 2 deletions frontend/setupChelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const setupChelonia = async (): Promise<*> => {
})

// this is to ensure compatibility between frontend and test/backend.test.js
sbp('okTurtles.data/set', 'API_URL', window.location.origin)
sbp('okTurtles.data/set', 'API_URL', self.location.origin)

// Used in 'chelonia/configure' hooks to emit an error notification.
const errorNotification = (activity: string, error: Error, message: GIMessage) => {
Expand Down Expand Up @@ -131,13 +131,14 @@ const setupChelonia = async (): Promise<*> => {
exposedGlobals: {
// note: needs to be written this way and not simply "Notification"
// because that breaks on mobile where Notification is undefined
Notification: window.Notification
Notification: self.Notification
}
}
},
hooks: {
handleEventError: (e: Error, message: GIMessage) => {
if (e.name === 'ChelErrorUnrecoverable') {
// TODO: Forward to app
sbp('gi.ui/seriousErrorBanner', e)
}
if (sbp('okTurtles.data/get', 'sideEffectError') !== message.hash()) {
Expand Down
2 changes: 1 addition & 1 deletion shared/domains/chelonia/GIMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export class GIMessage {

// $FlowFixMe[unsupported-syntax]
static [serdesSerializeSymbol] (m: GIMessage) {
return [m.serialize(), m.direction(), m.decryptedValue(), m.innerSigningKey()]
return [m.serialize(), m.direction(), m.decryptedValue(), m.innerSigningKeyId()]
}

// $FlowFixMe[unsupported-syntax]
Expand Down
1 change: 1 addition & 0 deletions shared/domains/chelonia/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const EXTERNALKM32 = 'externalkm32'

const bytesOrObjectToB64 = (ary: Uint8Array) => {
if (!(ary instanceof Uint8Array)) {
console.error('@@@bytesOrObjectToB64', ary, ary.constructor, ary instanceof Uint8Array)
throw Error('Unsupported type')
}
return bytesToB64(ary)
Expand Down
Loading

0 comments on commit 45eccd8

Please sign in to comment.