Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle frozen page lifecycle state #1658

Merged
merged 17 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composables/idb/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
3 changes: 3 additions & 0 deletions composables/masto/masto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions page-lifecycle.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions plugins/page-lifecycle.client.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
},
}
})
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion service-worker/notification.ts
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -104,3 +104,7 @@ function htmlToPlainText(html: string) {
return decodeURIComponent(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''))
}
*/

export function closeDatabaseConnections() {
closeDatabases()
}
12 changes: 11 additions & 1 deletion service-worker/web-push-notifications.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
/// <reference lib="WebWorker" />
/// <reference types="vite/client" />
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)
Expand Down
51 changes: 51 additions & 0 deletions utils/elk-idb.ts
Original file line number Diff line number Diff line change
@@ -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<T = any>(key: IDBValidKey) {
return getIdb<T>(key, defaultGetStore())
}

export function set(key: IDBValidKey, value: any) {
return setIdb(key, value, defaultGetStore())
}

export function update<T = any>(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
}