From 32cfe6371f1141f9445fe1055128ba5dad44a2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20S=C3=A1nchez?= Date: Mon, 6 Feb 2023 23:22:56 +0100 Subject: [PATCH] fix: handle frozen page lifecycle state (#1658) --- composables/idb/index.ts | 2 +- composables/masto/masto.ts | 3 ++ constants/index.ts | 2 + package.json | 1 + page-lifecycle.d.ts | 17 ++++++++ plugins/page-lifecycle.client.ts | 37 +++++++++++++++++ pnpm-lock.yaml | 6 +++ service-worker/notification.ts | 6 ++- service-worker/web-push-notifications.ts | 12 +++++- utils/elk-idb.ts | 51 ++++++++++++++++++++++++ 10 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 page-lifecycle.d.ts create mode 100644 plugins/page-lifecycle.client.ts create mode 100644 utils/elk-idb.ts diff --git a/composables/idb/index.ts b/composables/idb/index.ts index d71a30cc..e00396c7 100644 --- a/composables/idb/index.ts +++ b/composables/idb/index.ts @@ -1,7 +1,7 @@ import type { MaybeComputedRef, RemovableRef } from '@vueuse/core' import type { Ref } from 'vue' -import { del, get, set, update } from 'idb-keyval' import type { UseIDBOptions } from '@vueuse/integrations/useIDBKeyval' +import { del, get, set, update } from '~/utils/elk-idb' const isIDBSupported = !process.test && typeof indexedDB !== 'undefined' diff --git a/composables/masto/masto.ts b/composables/masto/masto.ts index d64addaa..f212a089 100644 --- a/composables/masto/masto.ts +++ b/composables/masto/masto.ts @@ -106,6 +106,9 @@ export function useStreaming( stream.value = cb(client.value) }) + if (process.client && !process.test) + useNuxtApp().$pageLifecycle.addFrozenListener(cleanup) + tryOnBeforeUnmount(() => isActive.value = false) if (controls) diff --git a/constants/index.ts b/constants/index.ts index 366946d0..c7d28132 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -3,6 +3,8 @@ export const APP_NAME = 'Elk' export const DEFAULT_POST_CHARS_LIMIT = 500 export const DEFAULT_FONT_SIZE = '15px' +export const ELK_PAGE_LIFECYCLE_FROZEN = 'elk-frozen' + export const STORAGE_KEY_DRAFTS = 'elk-drafts' export const STORAGE_KEY_USERS = 'elk-users' export const STORAGE_KEY_SERVERS = 'elk-servers' diff --git a/package.json b/package.json index baa33002..f46e2764 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "js-yaml": "^4.1.0", "lru-cache": "^7.14.1", "masto": "^5.6.1", + "page-lifecycle": "^0.1.2", "pinia": "^2.0.29", "shiki": "^0.12.1", "shiki-es": "^0.2.0", diff --git a/page-lifecycle.d.ts b/page-lifecycle.d.ts new file mode 100644 index 00000000..963a4bea --- /dev/null +++ b/page-lifecycle.d.ts @@ -0,0 +1,17 @@ +declare module 'page-lifecycle/dist/lifecycle.mjs' { + type PageLifecycleState = 'pageshow' | 'resume' | 'focus' | 'blur' | 'pagehide' | 'unload' | 'visibilitychange' | 'freeze' + + interface PageLifecycleEvent extends Event { + newState: PageLifecycleState + oldState: PageLifecycleState + } + interface PageLifecycle extends EventTarget { + get state(): PageLifecycleState + get pageWasDiscarded(): boolean + addUnsavedChanges: (id: Symbol | any) => void + removeUnsavedChanges: (id: Symbol | any) => void + addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void + } + const lifecycle: PageLifecycle + export default lifecycle +} diff --git a/plugins/page-lifecycle.client.ts b/plugins/page-lifecycle.client.ts new file mode 100644 index 00000000..8a8d88c1 --- /dev/null +++ b/plugins/page-lifecycle.client.ts @@ -0,0 +1,37 @@ +import lifecycle from 'page-lifecycle/dist/lifecycle.mjs' +import { ELK_PAGE_LIFECYCLE_FROZEN } from '~/constants' +import { closeDatabases } from '~/utils/elk-idb' + +export default defineNuxtPlugin(() => { + const state = ref(lifecycle.state) + const frozenListeners: (() => void)[] = [] + + lifecycle.addEventListener('statechange', (evt) => { + if (evt.newState === 'freeze') + frozenListeners.forEach(listener => listener()) + else + state.value = evt.newState + }) + + const addFrozenListener = (listener: () => void) => { + frozenListeners.push(listener) + } + + if (useAppConfig().pwaEnabled) { + addFrozenListener(() => { + if (navigator.serviceWorker.controller) + navigator.serviceWorker.controller.postMessage(ELK_PAGE_LIFECYCLE_FROZEN) + + closeDatabases() + }) + } + + return { + provide: { + pageLifecycle: reactive({ + state, + addFrozenListener, + }), + }, + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8533916e..85a204e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,7 @@ importers: nuxt: 3.1.1 nuxt-security: ^0.10.1 nuxt-vitest: ^0.6.4 + page-lifecycle: ^0.1.2 pinia: ^2.0.29 postcss-nested: ^6.0.0 prettier: ^2.8.3 @@ -155,6 +156,7 @@ importers: js-yaml: 4.1.0 lru-cache: 7.14.1 masto: 5.6.1 + page-lifecycle: 0.1.2 pinia: 2.0.29_typescript@4.9.5 shiki: 0.12.1 shiki-es: 0.2.0 @@ -9667,6 +9669,10 @@ packages: engines: {node: '>=6'} dev: true + /page-lifecycle/0.1.2: + resolution: {integrity: sha512-+3uccYgL0CXG0KSXRxZi4uc2E6mqFWV5HqiJJgcnaJCiS0LqiuJ4vB420N21NFuLvuvLB4Jr5drgQ2NXAXF9Iw==} + dev: false + /param-case/3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: diff --git a/service-worker/notification.ts b/service-worker/notification.ts index c585ac8d..82b34bd4 100644 --- a/service-worker/notification.ts +++ b/service-worker/notification.ts @@ -1,4 +1,4 @@ -import { get } from 'idb-keyval' +import { closeDatabases, get } from '../utils/elk-idb' import type { MastoNotification, NotificationInfo, PushPayload, UserLogin } from './types' export const findNotification = async ( @@ -104,3 +104,7 @@ function htmlToPlainText(html: string) { return decodeURIComponent(html.replace(//g, '\n').replace(/<\/p>

/g, '\n\n').replace(/<[^>]*>/g, '')) } */ + +export function closeDatabaseConnections() { + closeDatabases() +} diff --git a/service-worker/web-push-notifications.ts b/service-worker/web-push-notifications.ts index 43a48429..f1e66cc9 100644 --- a/service-worker/web-push-notifications.ts +++ b/service-worker/web-push-notifications.ts @@ -1,10 +1,20 @@ /// /// -import { createNotificationOptions, findNotification } from './notification' +import { ELK_PAGE_LIFECYCLE_FROZEN } from '../constants' +import { + closeDatabaseConnections, + createNotificationOptions, + findNotification, +} from './notification' import type { PushPayload } from '~/service-worker/types' declare const self: ServiceWorkerGlobalScope +self.addEventListener('message', (event) => { + if (event.data === ELK_PAGE_LIFECYCLE_FROZEN) + closeDatabaseConnections() +}) + export const onPush = (event: PushEvent) => { const promise = isClientFocused().then((isFocused) => { if (isFocused) diff --git a/utils/elk-idb.ts b/utils/elk-idb.ts new file mode 100644 index 00000000..d7363046 --- /dev/null +++ b/utils/elk-idb.ts @@ -0,0 +1,51 @@ +import { + type UseStore, + del as delIdb, + get as getIdb, + promisifyRequest, + set as setIdb, + update as updateIdb, +} from 'idb-keyval' + +const databases: IDBOpenDBRequest[] = [] + +function createStore(): UseStore { + const storeName = 'keyval' + const request = indexedDB.open('keyval-store') + databases.push(request) + request.onupgradeneeded = () => request.result.createObjectStore(storeName) + const dbp = promisifyRequest(request) + return (txMode, callback) => dbp.then(db => callback(db.transaction(storeName, txMode).objectStore(storeName))) +} + +let defaultGetStoreFunc: UseStore | undefined +function defaultGetStore() { + if (!defaultGetStoreFunc) + defaultGetStoreFunc = createStore() + + return defaultGetStoreFunc +} + +export function get(key: IDBValidKey) { + return getIdb(key, defaultGetStore()) +} + +export function set(key: IDBValidKey, value: any) { + return setIdb(key, value, defaultGetStore()) +} + +export function update(key: IDBValidKey, updater: (oldValue: T | undefined) => T) { + return updateIdb(key, updater, defaultGetStore()) +} + +export function del(key: IDBValidKey) { + return delIdb(key, defaultGetStore()) +} + +export function closeDatabases() { + databases.forEach((db) => { + if (db.result) + db.result.close() + }) + defaultGetStoreFunc = undefined +}